From 71648fba5ad6f5cfb19906a5ecf1d349a3d3f836 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 7 Oct 2026 14:10:25 +0000 Subject: [PATCH 1/6] Unify delimiter alignment across formatter syntax Use opener-relative, comma-leading layout for imports, exports, parenthesized terms, types and binders, and constraint lists. Preserve compact constructor-member spelling and fully expand wrapped exports. Fit complete superclass headers before breaking their lists below class, keeping the closing parenthesis, arrow, and class head together when possible. Preserve comment boundaries and exclude class bodies from the fit decision. Include width-boundary, exact class-head fit, comment, constructor-member, and binder fixtures with regenerated expectations. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-01a11570-3ec2-7539-bc19-3d4825503ee1 --- compiler-frontend/formatting/src/printer.rs | 205 ++++++++++-------- .../1791034641_width_aware/Main.snap | 66 +++--- .../Main.snap | 66 +++--- .../1791044520_delimited_do_headers/Main.purs | 3 + .../1791044520_delimited_do_headers/Main.snap | 107 ++++++--- .../Main.purs | 7 +- .../Main.snap | 112 ++++++++-- .../Main.snap | 26 +-- .../Main.snap | 58 +++-- .../1791047940_syntax_boundaries/Main.purs | 2 +- .../1791047940_syntax_boundaries/Main.snap | 12 +- .../Main.snap | 7 +- .../Main.snap | 8 +- .../Main.snap | 26 +-- .../1791199200_sorted_imports/Main.snap | 20 +- .../Main.purs | 13 ++ .../Main.snap | 100 +++++++++ 17 files changed, 538 insertions(+), 300 deletions(-) create mode 100644 tests-integration/fixtures/formatting/1791361260_import_export_list_alignment/Main.purs create mode 100644 tests-integration/fixtures/formatting/1791361260_import_export_list_alignment/Main.snap diff --git a/compiler-frontend/formatting/src/printer.rs b/compiler-frontend/formatting/src/printer.rs index 6b98a14db..f1357ddb6 100644 --- a/compiler-frontend/formatting/src/printer.rs +++ b/compiler-frontend/formatting/src/printer.rs @@ -425,7 +425,16 @@ impl<'arena> Printer<'arena, '_> { if tail.is_empty() { return self.fixed(head, context); } - let head_document = self.fixed(head, Context { following: Some(margin), ..context })?; + let head_context = Context { following: Some(margin), ..context }; + let head_document = if let [keyword, constraints] = head + && keyword.kind == CLASS + && constraints.kind == ClassConstraints + && constraints.children.first().is_some_and(|tree| tree.kind == LEFT_PARENTHESIS) + { + self.delimited(constraints, head_context, Some((keyword, tail[0])))? + } else { + self.fixed(head, head_context)? + }; let gap = |position: usize| { let previous = if position == 0 { head.last().unwrap() } else { tail[position - 1] }; let element = tail[position]; @@ -437,7 +446,10 @@ impl<'arena> Printer<'arena, '_> { }; let fixed_gaps = (0..tail.len()).all(|position| !matches!(gap(position), Gap::Soft | Gap::SoftEmpty)); - if fixed_gaps || elements.iter().any(|tree| tree.has_inline_do()) { + if fixed_gaps + || elements.iter().any(|tree| tree.has_inline_do()) + || head.last().is_some_and(|tree| tree.kind == ClassConstraints) + { let mut inline_suffix = self.arena.nil(); let mut broken_suffix = self.arena.nil(); for position in (0..tail.len()).rev() { @@ -473,10 +485,11 @@ impl<'arena> Printer<'arena, '_> { fn delimited( &self, - elements: &[&Tree], + tree: &Tree, context: Context, - aligned: bool, + prefix: Option<(&Tree, &Tree)>, ) -> Result, FormatError> { + let elements = tree.elements(); let opening = elements .iter() .position(|tree| matches!(tree.kind, LEFT_PARENTHESIS | LEFT_SQUARE | LEFT_CURLY)); @@ -484,102 +497,103 @@ impl<'arena> Printer<'arena, '_> { .iter() .rposition(|tree| matches!(tree.kind, RIGHT_PARENTHESIS | RIGHT_SQUARE | RIGHT_CURLY)); let Some((opening, closing)) = opening.zip(closing) else { - return self.fixed(elements, context); + return self.fixed(&elements, context); }; - if closing == opening + 1 && !aligned { - return self.fixed(elements, context); - } - if aligned { - // Coordinates are relative to the opener, including inline openers. - // Items start after the two-column `{ ` or `, ` prefix; only their - // continuations use the configured indentation width. - let item_context = Context { margin: 2, following: None }; - let punctuation_context = Context { margin: 0, following: None }; - let mut document = self.tree(elements[opening], punctuation_context)?; - let items = elements[opening + 1..closing].iter().flat_map(|element| { - if element.kind == TypeRowTail { element.elements() } else { vec![*element] } - }); - let items = items.collect::>(); - for (position, element) in items.iter().enumerate() { - let punctuation = matches!(element.kind, COMMA | PIPE); - let gap = if position == 0 { - if elements[opening].kind == LEFT_PARENTHESIS { - Gap::BrokenSpace - } else { - Gap::Space - } - } else if element.kind == COMMA { - Gap::SoftEmpty - } else if element.kind == PIPE { - Gap::Soft - } else { - self.fixed_gap(element.start) - }; - let boundary = self.boundary(element.start, gap)?; - let body = if punctuation { - boundary.append(self.tree(element, punctuation_context)?) - } else if position > 0 && items[position - 1].kind == PIPE { - // A row tail can start beside the opener or after a comment, - // so its continuation must follow the type's actual column. - boundary.append(self.tree(element, punctuation_context)?.align()).nest(2) + // Coordinates are relative to the opener, including inline openers. + // Items start after the two-column `{ ` or `, ` prefix; only their + // continuations use the configured indentation width. + let item_context = Context { margin: 2, following: None }; + let punctuation_context = Context { margin: 0, following: None }; + let mut document = self.tree(elements[opening], punctuation_context)?; + let items = elements[opening + 1..closing].iter().flat_map(|element| { + if element.kind == TypeRowTail { element.elements() } else { vec![*element] } + }); + let items = items.collect::>(); + for (position, element) in items.iter().enumerate() { + let punctuation = matches!(element.kind, COMMA | PIPE); + let gap = if position == 0 { + if elements[opening].kind == LEFT_PARENTHESIS { + Gap::BrokenSpace } else { - boundary.append(self.tree(element, item_context)?).nest(2) - }; - document = document.append(body); - } - let edge = if items.is_empty() { - Gap::Tight - } else if matches!(elements[opening].kind, LEFT_CURLY | LEFT_SQUARE) { + Gap::Space + } + } else if element.kind == COMMA { + Gap::SoftEmpty + } else if element.kind == PIPE { Gap::Soft } else { - Gap::SoftEmpty + self.fixed_gap(element.start) }; - document = document - .append(self.boundary(elements[closing].start, edge)?) - .append(self.tree(elements[closing], punctuation_context)?) - .group() - .align(); - return Ok(document); - } - let edge = if elements[opening].kind == LEFT_CURLY { Gap::Soft } else { Gap::SoftEmpty }; - let inner_context = - Context { margin: context.margin + self.config.indent_width, following: None }; - let body = - self.sequence(&elements[opening + 1..closing], inner_context, |_, _, current| { - if current.kind == COMMA { - Gap::SoftEmpty - } else if current.kind == TypeRowTail { - Gap::Soft - } else { - self.fixed_gap(current.start) - } - })?; - let opening_document = - self.fixed(&elements[..=opening], Context { following: None, ..context })?; - let boundary = self.boundary(elements[opening + 1].start, edge)?; - let boundary = if elements[opening + 1..closing].iter().any(|tree| tree.has_inline_do()) { - boundary.group() + let boundary = self.boundary(element.start, gap)?; + let body = if punctuation { + boundary.append(self.tree(element, punctuation_context)?) + } else if position > 0 && items[position - 1].kind == PIPE { + // A row tail can start beside the opener or after a comment, + // so its continuation must follow the type's actual column. + boundary.append(self.tree(element, punctuation_context)?.align()).nest(2) + } else { + boundary.append(self.tree(element, item_context)?).nest(2) + }; + document = document.append(body); + } + let edge = if items.is_empty() { + Gap::Tight + } else if matches!(elements[opening].kind, LEFT_CURLY | LEFT_SQUARE) { + Gap::Soft } else { - boundary + Gap::SoftEmpty }; - let document = opening_document - .append(boundary.append(body).nest(self.config.indent_width as isize)) + document = document .append(self.boundary(elements[closing].start, edge)?) - .append(self.tree(elements[closing], context)?) - .group(); + .append(self.tree(elements[closing], punctuation_context)?); + if prefix.is_none() && !matches!(tree.kind, ExportList | TypeItemsList) { + document = document.group(); + } + let mut document = document.align(); + if let Some((prefix, head)) = prefix { + let keyword = self.tree(prefix, context)?; + let broken = keyword.clone().append( + self.boundary(elements[opening].start, Gap::Hard(1))? + .append(document.clone()) + .nest(self.config.indent_width as isize), + ); + document = keyword.append( + self.boundary(elements[opening].start, Gap::Soft)? + .append(document) + .nest(self.config.indent_width as isize), + ); + // Fit the complete class head before allowing its parameters to wrap. + let probe = + self.arena.fail().flat_alt(self.fixed(&[prefix, tree, head], context)?).group(); + let width = self.config.line_width; + let inline = document.group(); + document = self.arena.column(move |column| { + let mut rendered = String::new(); + if probe.render_fmt(width.saturating_sub(column), &mut rendered).is_ok() { + inline.clone().into_doc() + } else { + broken.clone().into_doc() + } + }); + } + if opening > 0 { + document = self + .fixed(&elements[..opening], context)? + .append(self.boundary(elements[opening].start, Gap::Space)?) + .append(document); + } if closing + 1 < elements.len() { let margin = context.margin + self.config.indent_width; - Ok(document.append( + document = document.append( self.boundary( elements[closing + 1].start, self.fixed_gap(elements[closing + 1].start), )? .append(self.fixed(&elements[closing + 1..], Context { margin, ..context })?) .nest(self.config.indent_width as isize), - )) - } else { - Ok(document) + ); } + Ok(document) } fn arrows(&self, tree: &Tree, context: Context) -> Result, FormatError> { @@ -733,6 +747,16 @@ impl<'arena> Printer<'arena, '_> { match tree.kind { Module => self.sequence(&elements, context, |_, _, _| Gap::Hard(2)), ModuleImports | ModuleStatements => self.statements(&elements, context), + ImportType | ExportType => { + let document = self.sequence(&elements, context, |_, _, current| { + if current.kind == TypeItemsList { + Gap::BrokenSpace + } else { + self.fixed_gap(current.start) + } + })?; + Ok(document.group()) + } DoStatements | CaseBranches | LetBindingStatements | ClassStatements | InstanceStatements | Conditionals => { // A layout body must exceed the planned following continuation, @@ -807,16 +831,21 @@ impl<'arena> Printer<'arena, '_> { .append(self.fixed(&elements[position + 1..], context)?) .group()) } - ExpressionArray | ExpressionRecord | BinderArray | BinderRecord | TypeRecord - | TypeRow | RecordUpdates => self.delimited(&elements, context, true), - ExpressionParenthesized + ExpressionArray + | ExpressionRecord + | BinderArray + | BinderRecord + | TypeRecord + | TypeRow + | RecordUpdates + | ExpressionParenthesized | BinderParenthesized | TypeParenthesized | ExportList | ImportList | TypeItemsList | InstanceConstraints - | ClassConstraints => self.delimited(&elements, context, false), + | ClassConstraints => self.delimited(tree, context, None), ExpressionIfThenElse => self.conditional(&elements, context), ValueEquation | LetBindingEquation @@ -857,7 +886,7 @@ impl<'arena> Printer<'arena, '_> { | TypeSynonymEquation | NewtypeEquation => { if matches!(elements.first().map(|tree| tree.kind), Some(LEFT_PARENTHESIS)) { - return self.delimited(&elements, context, false); + return self.delimited(tree, context, None); } let separator = elements.iter().position(|tree| { matches!( diff --git a/tests-integration/fixtures/formatting/1791034641_width_aware/Main.snap b/tests-integration/fixtures/formatting/1791034641_width_aware/Main.snap index 91a92f3e5..3baa21ea4 100644 --- a/tests-integration/fixtures/formatting/1791034641_width_aware/Main.snap +++ b/tests-integration/fixtures/formatting/1791034641_width_aware/Main.snap @@ -1,22 +1,20 @@ --- source: tests-integration/tests/formatting.rs -assertion_line: 118 +assertion_line: 127 --- === width 40, indent 2 === module Main - ( - longApplication - , longRecord - , branches - , Box(..) + ( longApplication + , longRecord + , branches + , Box(..) ) where import Data.Collections - ( - Collection - , mapCollection - , filterCollection + ( Collection + , mapCollection + , filterCollection ) as Collections @@ -425,19 +423,17 @@ last === width 40, indent 1 === module Main - ( - longApplication - , longRecord - , branches - , Box(..) + ( longApplication + , longRecord + , branches + , Box(..) ) where import Data.Collections - ( - Collection - , mapCollection - , filterCollection + ( Collection + , mapCollection + , filterCollection ) as Collections @@ -604,19 +600,17 @@ last === width 40, indent 4 === module Main - ( - longApplication - , longRecord - , branches - , Box(..) + ( longApplication + , longRecord + , branches + , Box(..) ) where import Data.Collections - ( - Collection - , mapCollection - , filterCollection + ( Collection + , mapCollection + , filterCollection ) as Collections @@ -789,19 +783,17 @@ last === width 40, indent 2, unicode === module Main - ( - longApplication - , longRecord - , branches - , Box(..) + ( longApplication + , longRecord + , branches + , Box(..) ) where import Data.Collections - ( - Collection - , mapCollection - , filterCollection + ( Collection + , mapCollection + , filterCollection ) as Collections diff --git a/tests-integration/fixtures/formatting/1791042120_alternate_source_wrapping/Main.snap b/tests-integration/fixtures/formatting/1791042120_alternate_source_wrapping/Main.snap index 7ddebde15..b187966fd 100644 --- a/tests-integration/fixtures/formatting/1791042120_alternate_source_wrapping/Main.snap +++ b/tests-integration/fixtures/formatting/1791042120_alternate_source_wrapping/Main.snap @@ -1,22 +1,20 @@ --- source: tests-integration/tests/formatting.rs -assertion_line: 118 +assertion_line: 127 --- === width 40, indent 2 === module Main - ( - longApplication - , longRecord - , branches - , Box(..) + ( longApplication + , longRecord + , branches + , Box(..) ) where import Data.Collections - ( - Collection - , mapCollection - , filterCollection + ( Collection + , mapCollection + , filterCollection ) as Collections @@ -415,19 +413,17 @@ last === width 40, indent 1 === module Main - ( - longApplication - , longRecord - , branches - , Box(..) + ( longApplication + , longRecord + , branches + , Box(..) ) where import Data.Collections - ( - Collection - , mapCollection - , filterCollection + ( Collection + , mapCollection + , filterCollection ) as Collections @@ -590,19 +586,17 @@ last === width 40, indent 4 === module Main - ( - longApplication - , longRecord - , branches - , Box(..) + ( longApplication + , longRecord + , branches + , Box(..) ) where import Data.Collections - ( - Collection - , mapCollection - , filterCollection + ( Collection + , mapCollection + , filterCollection ) as Collections @@ -771,19 +765,17 @@ last === width 40, indent 2, unicode === module Main - ( - longApplication - , longRecord - , branches - , Box(..) + ( longApplication + , longRecord + , branches + , Box(..) ) where import Data.Collections - ( - Collection - , mapCollection - , filterCollection + ( Collection + , mapCollection + , filterCollection ) as Collections diff --git a/tests-integration/fixtures/formatting/1791044520_delimited_do_headers/Main.purs b/tests-integration/fixtures/formatting/1791044520_delimited_do_headers/Main.purs index 66f764179..cb52dc73c 100644 --- a/tests-integration/fixtures/formatting/1791044520_delimited_do_headers/Main.purs +++ b/tests-integration/fixtures/formatting/1791044520_delimited_do_headers/Main.purs @@ -20,3 +20,6 @@ applicative = use (ado first <- firstAction second <- secondAction in combine first second) + +wrapped = (combine firstArgument secondArgument) +nested = ((combine firstArgument secondArgument)) diff --git a/tests-integration/fixtures/formatting/1791044520_delimited_do_headers/Main.snap b/tests-integration/fixtures/formatting/1791044520_delimited_do_headers/Main.snap index b805991d3..60c6bdcd9 100644 --- a/tests-integration/fixtures/formatting/1791044520_delimited_do_headers/Main.snap +++ b/tests-integration/fixtures/formatting/1791044520_delimited_do_headers/Main.snap @@ -1,18 +1,18 @@ --- source: tests-integration/tests/formatting.rs -assertion_line: 114 +assertion_line: 127 --- === width 30, indent 2 === module Main where -lambda = for_ values ( - \value -> do - log value -) +lambda = for_ values + ( \value -> do + log value + ) -parenthesized = use (do - action -) next +parenthesized = use ( do + action + ) next record = { onClick: do action @@ -25,22 +25,39 @@ array = [ do secondAction ] -applicative = use (ado - first <- firstAction - second <- secondAction - in combine first second -) +applicative = use ( ado + first <- + firstAction + second <- + secondAction + in + combine + first + second + ) + +wrapped = + ( combine + firstArgument + secondArgument + ) +nested = + ( ( combine + firstArgument + secondArgument + ) + ) === width 80, indent 2 === module Main where -lambda = for_ values (\value -> do - log value -) +lambda = for_ values ( \value -> do + log value + ) -parenthesized = use (do - action -) next +parenthesized = use ( do + action + ) next record = { onClick: do action @@ -53,22 +70,25 @@ array = [ do secondAction ] -applicative = use (ado - first <- firstAction - second <- secondAction - in combine first second -) +applicative = use ( ado + first <- firstAction + second <- secondAction + in combine first second + ) + +wrapped = (combine firstArgument secondArgument) +nested = ((combine firstArgument secondArgument)) === width 40, indent 4, unicode === module Main where -lambda = for_ values (\value → do - log value -) +lambda = for_ values ( \value → do + log value + ) -parenthesized = use (do - action -) next +parenthesized = use ( do + action + ) next record = { onClick: do action @@ -81,8 +101,25 @@ array = [ do secondAction ] -applicative = use (ado - first ← firstAction - second ← secondAction - in combine first second -) +applicative = use ( ado + first ← + firstAction + second ← + secondAction + in + combine + first + second + ) + +wrapped = + ( combine + firstArgument + secondArgument + ) +nested = + ( ( combine + firstArgument + secondArgument + ) + ) diff --git a/tests-integration/fixtures/formatting/1791046620_constraint_continuation_comments/Main.purs b/tests-integration/fixtures/formatting/1791046620_constraint_continuation_comments/Main.purs index e868dc57c..812aa91e6 100644 --- a/tests-integration/fixtures/formatting/1791046620_constraint_continuation_comments/Main.purs +++ b/tests-integration/fixtures/formatting/1791046620_constraint_continuation_comments/Main.purs @@ -1,6 +1,9 @@ -- @format width=20 indent=1 +-- @format width=30 -- @format width=80 -- @format width=40 indent=4 unicode=true +-- @format width=41 indent=4 unicode=true +-- @format width=42 indent=4 unicode=true module Main where instance (Show value) -- instance explanation @@ -10,6 +13,8 @@ derive instance (Eq value) -- derive explanation => Eq (Box value) -class (Eq value) +class (Eq value, Show value) -- superclass explanation <= Ord value + +class (Eq value, Show value) <= Thing value diff --git a/tests-integration/fixtures/formatting/1791046620_constraint_continuation_comments/Main.snap b/tests-integration/fixtures/formatting/1791046620_constraint_continuation_comments/Main.snap index 2c824ff8f..4571f4e78 100644 --- a/tests-integration/fixtures/formatting/1791046620_constraint_continuation_comments/Main.snap +++ b/tests-integration/fixtures/formatting/1791046620_constraint_continuation_comments/Main.snap @@ -1,25 +1,56 @@ --- source: tests-integration/tests/formatting.rs -assertion_line: 106 +assertion_line: 127 --- === width 20, indent 1 === module Main where -instance ( - Show value -) -- instance explanation +instance ( Show + value + ) -- instance explanation => Show (Box value) -derive instance ( - Eq value -) +derive instance ( Eq + value + ) -- derive explanation => Eq (Box value) -class (Eq value) +class + ( Eq value + , Show value + ) -- superclass explanation <= Ord value +class + ( Eq value + , Show value + ) <= Thing value + +=== width 30, indent 2 === +module Main where + +instance ( Show value + ) -- instance explanation + => Show (Box value) + +derive instance (Eq value) + -- derive explanation + => Eq (Box value) + +class + ( Eq value + , Show value + ) + -- superclass explanation + <= Ord value + +class + ( Eq value + , Show value + ) <= Thing value + === width 80, indent 2 === module Main where @@ -30,22 +61,77 @@ derive instance (Eq value) -- derive explanation => Eq (Box value) -class (Eq value) +class + ( Eq value + , Show value + ) -- superclass explanation <= Ord value +class (Eq value, Show value) <= Thing value + === width 40, indent 4, unicode === module Main where -instance ( - Show value -) -- instance explanation +instance ( Show value + ) -- instance explanation ⇒ Show (Box value) derive instance (Eq value) -- derive explanation ⇒ Eq (Box value) -class (Eq value) +class + ( Eq value + , Show value + ) -- superclass explanation ⇐ Ord value + +class + ( Eq value + , Show value + ) ⇐ Thing value + +=== width 41, indent 4, unicode === +module Main where + +instance ( Show value + ) -- instance explanation + ⇒ Show (Box value) + +derive instance (Eq value) + -- derive explanation + ⇒ Eq (Box value) + +class + ( Eq value + , Show value + ) + -- superclass explanation + ⇐ Ord value + +class + ( Eq value + , Show value + ) ⇐ Thing value + +=== width 42, indent 4, unicode === +module Main where + +instance ( Show value + ) -- instance explanation + ⇒ Show (Box value) + +derive instance (Eq value) + -- derive explanation + ⇒ Eq (Box value) + +class + ( Eq value + , Show value + ) + -- superclass explanation + ⇐ Ord value + +class (Eq value, Show value) ⇐ Thing value diff --git a/tests-integration/fixtures/formatting/1791046620_declaration_separators/Main.snap b/tests-integration/fixtures/formatting/1791046620_declaration_separators/Main.snap index 4762b1375..4584a4519 100644 --- a/tests-integration/fixtures/formatting/1791046620_declaration_separators/Main.snap +++ b/tests-integration/fixtures/formatting/1791046620_declaration_separators/Main.snap @@ -1,6 +1,6 @@ --- source: tests-integration/tests/formatting.rs -assertion_line: 118 +assertion_line: 127 --- === width 40, indent 2 === module Main where @@ -36,13 +36,11 @@ class Multiple | firstLongVariable secondLongVariable -> thirdLongVariable fourthLongVariable -instance command :: ( - Show configuration - , Eq environment -) => +instance command :: ( Show configuration + , Eq environment + ) => Show - ( - Command configuration environment + ( Command configuration environment ) where show value = "command" @@ -112,14 +110,12 @@ class Multiple | firstLongVariable secondLongVariable → thirdLongVariable fourthLongVariable -instance command ∷ ( - Show configuration - , Eq environment -) ⇒ +instance command ∷ ( Show configuration + , Eq environment + ) ⇒ Show - ( - Command - configuration - environment + ( Command + configuration + environment ) where show value = "command" diff --git a/tests-integration/fixtures/formatting/1791046620_tight_syntax_at_line_starts/Main.snap b/tests-integration/fixtures/formatting/1791046620_tight_syntax_at_line_starts/Main.snap index d51679b9c..25f9def97 100644 --- a/tests-integration/fixtures/formatting/1791046620_tight_syntax_at_line_starts/Main.snap +++ b/tests-integration/fixtures/formatting/1791046620_tight_syntax_at_line_starts/Main.snap @@ -1,25 +1,22 @@ --- source: tests-integration/tests/formatting.rs -assertion_line: 118 +assertion_line: 127 --- === width 30, indent 4 === module Main where main = traverse_ - ( - \item -> - log - ( - "processing " - <> show - item - ) + ( \item -> + log + ( "processing " + <> show + item + ) ) - ( - Array.range - 1 - someUpperBoundValue + ( Array.range + 1 + someUpperBoundValue ) terms = do @@ -28,9 +25,9 @@ terms = do (-1) (h @b) ({ a: 1 }) - ((a do - b - ) + ( ( a do + b + ) ) patterns value = @@ -58,7 +55,7 @@ terms = do (-1) (h @b) ({ a: 1 }) - ((a do + ( ( a do b ) ) @@ -76,19 +73,16 @@ module Main where main = traverse_ - ( - \item → - log - ( - "processing " - <> show - item - ) + ( \item → + log + ( "processing " + <> show + item + ) ) - ( - Array.range - 1 - someUpperBoundValue + ( Array.range + 1 + someUpperBoundValue ) terms = do @@ -97,9 +91,9 @@ terms = do (-1) (h @b) ({ a: 1 }) - ((a do - b - ) + ( ( a do + b + ) ) patterns value = diff --git a/tests-integration/fixtures/formatting/1791047940_syntax_boundaries/Main.purs b/tests-integration/fixtures/formatting/1791047940_syntax_boundaries/Main.purs index 47d57f9e5..686e0d153 100644 --- a/tests-integration/fixtures/formatting/1791047940_syntax_boundaries/Main.purs +++ b/tests-integration/fixtures/formatting/1791047940_syntax_boundaries/Main.purs @@ -67,7 +67,7 @@ emptyAdo = ado in 1 hole = ?hole binders {} [] named@value (item :: Int) { field: field } = value -binderOperator (head : tail) = head +binderOperator (firstElement : remainingElements) = firstElement whereValue = result where result = 1 diff --git a/tests-integration/fixtures/formatting/1791047940_syntax_boundaries/Main.snap b/tests-integration/fixtures/formatting/1791047940_syntax_boundaries/Main.snap index b8d827b77..d796e2e20 100644 --- a/tests-integration/fixtures/formatting/1791047940_syntax_boundaries/Main.snap +++ b/tests-integration/fixtures/formatting/1791047940_syntax_boundaries/Main.snap @@ -1,6 +1,6 @@ --- source: tests-integration/tests/formatting.rs -assertion_line: 118 +assertion_line: 127 --- === width 40, indent 2 === module Main where @@ -89,7 +89,9 @@ binders {} named@value (item :: Int) { field: field } = value -binderOperator (head : tail) = head +binderOperator ( firstElement + : remainingElements + ) = firstElement whereValue = result where result = 1 @@ -182,7 +184,7 @@ emptyAdo = ado in 1 hole = ?hole binders {} [] named@value (item :: Int) { field: field } = value -binderOperator (head : tail) = head +binderOperator (firstElement : remainingElements) = firstElement whereValue = result where result = 1 @@ -284,7 +286,9 @@ binders {} named@value (item ∷ Int) { field: field } = value -binderOperator (head : tail) = head +binderOperator ( firstElement + : remainingElements + ) = firstElement whereValue = result where result = 1 diff --git a/tests-integration/fixtures/formatting/1791093360_leading_comma_layout_boundaries/Main.snap b/tests-integration/fixtures/formatting/1791093360_leading_comma_layout_boundaries/Main.snap index 7c90bac13..ac94b9b6c 100644 --- a/tests-integration/fixtures/formatting/1791093360_leading_comma_layout_boundaries/Main.snap +++ b/tests-integration/fixtures/formatting/1791093360_leading_comma_layout_boundaries/Main.snap @@ -1,6 +1,6 @@ --- source: tests-integration/tests/formatting.rs -assertion_line: 118 +assertion_line: 127 --- === width 20, indent 1 === module Main where @@ -130,9 +130,8 @@ accessUpdate = secondArgument } parenthesizedUpdate = - ( - chooseRecord - firstArgument + ( chooseRecord + firstArgument ) { field = firstArgument diff --git a/tests-integration/fixtures/formatting/1791134340_leading_operator_operands/Main.snap b/tests-integration/fixtures/formatting/1791134340_leading_operator_operands/Main.snap index 8ac403410..23ca2eb88 100644 --- a/tests-integration/fixtures/formatting/1791134340_leading_operator_operands/Main.snap +++ b/tests-integration/fixtures/formatting/1791134340_leading_operator_operands/Main.snap @@ -1,6 +1,6 @@ --- source: tests-integration/tests/formatting.rs -assertion_line: 118 +assertion_line: 127 --- === width 20, indent 2 === module Main where @@ -63,8 +63,7 @@ commented = secondArgument negated = firstLongOperand - - ( - -secondLongOperand + - ( -secondLongOperand ) qualified = @@ -214,8 +213,7 @@ commented = secondArgument negated = firstLongOperand - - ( - -secondLongOperand + - ( -secondLongOperand ) qualified = diff --git a/tests-integration/fixtures/formatting/1791134340_nested_backtick_spacing/Main.snap b/tests-integration/fixtures/formatting/1791134340_nested_backtick_spacing/Main.snap index 18c8425ed..d4f28d58c 100644 --- a/tests-integration/fixtures/formatting/1791134340_nested_backtick_spacing/Main.snap +++ b/tests-integration/fixtures/formatting/1791134340_nested_backtick_spacing/Main.snap @@ -1,6 +1,6 @@ --- source: tests-integration/tests/formatting.rs -assertion_line: 118 +assertion_line: 127 --- === width 20, indent 2 === module Main where @@ -8,18 +8,15 @@ module Main where value = firstLongOperand `combine - ( - inner + ( inner `f` argument )` secondLongOperand nested = firstOperand `combine - ( - inner + ( inner `f - ( - deeper + ( deeper `g` argument )` another )` secondOperand @@ -37,17 +34,14 @@ module Main where value = firstLongOperand `combine - ( - inner `f` argument + ( inner `f` argument )` secondLongOperand nested = firstOperand `combine - ( - inner - `f - ( - deeper - `g` argument - )` another + ( inner + `f + ( deeper + `g` argument + )` another )` secondOperand diff --git a/tests-integration/fixtures/formatting/1791199200_sorted_imports/Main.snap b/tests-integration/fixtures/formatting/1791199200_sorted_imports/Main.snap index 04f899eac..10bf70ee1 100644 --- a/tests-integration/fixtures/formatting/1791199200_sorted_imports/Main.snap +++ b/tests-integration/fixtures/formatting/1791199200_sorted_imports/Main.snap @@ -1,6 +1,6 @@ --- source: tests-integration/tests/formatting.rs -assertion_line: 124 +assertion_line: 127 --- === width 80, indent 2 === module Main where -- Module explanation. @@ -37,8 +37,7 @@ import Data.Maybe (Maybe(..)) as Maybe import Data.Tuple - ( - Tuple(..) + ( Tuple(..) ) {- Tuple explanation. Still attached to Tuple. -} import Prelude @@ -51,12 +50,10 @@ import Prelude -- The declaration explanation. test = Tuple - ( - Maybe.Just - ( - Functions.identity - 1 - ) + ( Maybe.Just + ( Functions.identity + 1 + ) ) (length [ 1, 2 ]) @@ -80,8 +77,7 @@ import Prelude as Functions -- The declaration explanation. test = Tuple - ( - Maybe.Just - (Functions.identity 1) + ( Maybe.Just + (Functions.identity 1) ) (length [ 1, 2 ]) diff --git a/tests-integration/fixtures/formatting/1791361260_import_export_list_alignment/Main.purs b/tests-integration/fixtures/formatting/1791361260_import_export_list_alignment/Main.purs new file mode 100644 index 000000000..c08250814 --- /dev/null +++ b/tests-integration/fixtures/formatting/1791361260_import_export_list_alignment/Main.purs @@ -0,0 +1,13 @@ +-- @format width=52 +-- @format width=53 +-- @format width=80 +-- @format width=30 indent=4 +-- @format width=20 indent=4 +module Main (first, second, Box(First, Second)) where + +import Library (first, second, Box(First, Second)) +import Library hiding (first, second, Box(First, Second)) as Hidden + +first = 1 +second = 2 +data Box = First | Second diff --git a/tests-integration/fixtures/formatting/1791361260_import_export_list_alignment/Main.snap b/tests-integration/fixtures/formatting/1791361260_import_export_list_alignment/Main.snap new file mode 100644 index 000000000..2ec187ea6 --- /dev/null +++ b/tests-integration/fixtures/formatting/1791361260_import_export_list_alignment/Main.snap @@ -0,0 +1,100 @@ +--- +source: tests-integration/tests/formatting.rs +assertion_line: 127 +--- +=== width 52, indent 2 === +module Main + ( first + , second + , Box(First, Second) + ) + where + +import Library (first, second, Box(First, Second)) +import Library + hiding (first, second, Box(First, Second)) + as Hidden + +first = 1 +second = 2 +data Box = First | Second + +=== width 53, indent 2 === +module Main (first, second, Box(First, Second)) where + +import Library (first, second, Box(First, Second)) +import Library + hiding (first, second, Box(First, Second)) + as Hidden + +first = 1 +second = 2 +data Box = First | Second + +=== width 80, indent 2 === +module Main (first, second, Box(First, Second)) where + +import Library (first, second, Box(First, Second)) +import Library hiding (first, second, Box(First, Second)) as Hidden + +first = 1 +second = 2 +data Box = First | Second + +=== width 30, indent 4 === +module Main + ( first + , second + , Box(First, Second) + ) + where + +import Library + ( first + , second + , Box(First, Second) + ) +import Library + hiding ( first + , second + , Box ( First + , Second + ) + ) + as Hidden + +first = 1 +second = 2 +data Box = First | Second + +=== width 20, indent 4 === +module Main + ( first + , second + , Box ( First + , Second + ) + ) + where + +import Library + ( first + , second + , Box ( First + , Second + ) + ) +import Library + hiding ( first + , second + , Box ( First + , Second + ) + ) + as Hidden + +first = 1 +second = 2 +data Box = + First + | Second From 1dcb991d731ac61f6766793e5d2928e894d8c382 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 7 Oct 2026 14:13:19 +0000 Subject: [PATCH 2/6] Align data alternatives and constructor arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Place a broken data equation’s leading equals sign alongside its alternative pipes. Anchor constructor arguments to the constructor column instead of the declaration margin. Regenerate declaration, record-constructor, and commented-separator expectations at the existing widths and indentation settings. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-01a11570-3ec2-7539-bc19-3d4825503ee1 --- compiler-frontend/formatting/src/printer.rs | 8 +++---- .../1791034641_width_aware/Main.snap | 16 ++++++------- .../Main.snap | 16 ++++++------- .../Main.snap | 12 +++++----- .../1791048960_commented_separators/Main.snap | 14 +++++------ .../Main.snap | 24 +++++++++---------- .../Main.snap | 4 ++-- 7 files changed, 47 insertions(+), 47 deletions(-) diff --git a/compiler-frontend/formatting/src/printer.rs b/compiler-frontend/formatting/src/printer.rs index f1357ddb6..67b8a97d9 100644 --- a/compiler-frontend/formatting/src/printer.rs +++ b/compiler-frontend/formatting/src/printer.rs @@ -770,10 +770,10 @@ impl<'arena> Printer<'arena, '_> { | ExpressionRecordUpdate | RecordUpdateBranch | BinderConstructor - | DataConstructor | InstanceHead | ClassHead | FunctionBinders => self.application(&elements, 1, context), + DataConstructor => Ok(self.application(&elements, 1, context)?.align()), ExpressionOperatorChain | ExpressionInfixChain | TypeOperatorChain @@ -808,11 +808,11 @@ impl<'arena> Printer<'arena, '_> { if let Some(separator) = elements.iter().position(|tree| tree.kind == EQUAL) { self.chain( &elements, - separator + 1, + separator, context, context.margin + self.config.indent_width, - |_, previous, current| { - if current.kind == PIPE || previous.kind == EQUAL { + |_, _, current| { + if matches!(current.kind, EQUAL | PIPE) { Gap::Soft } else { self.fixed_gap(current.start) diff --git a/tests-integration/fixtures/formatting/1791034641_width_aware/Main.snap b/tests-integration/fixtures/formatting/1791034641_width_aware/Main.snap index 3baa21ea4..f5ea844c3 100644 --- a/tests-integration/fixtures/formatting/1791034641_width_aware/Main.snap +++ b/tests-integration/fixtures/formatting/1791034641_width_aware/Main.snap @@ -18,8 +18,8 @@ import Data.Collections ) as Collections -data Box value = - Box value +data Box value + = Box value | Many value value value longApplication :: @@ -437,8 +437,8 @@ import Data.Collections ) as Collections -data Box value = - Box value +data Box value + = Box value | Many value value value longApplication :: @@ -614,8 +614,8 @@ import Data.Collections ) as Collections -data Box value = - Box value +data Box value + = Box value | Many value value value longApplication :: @@ -797,8 +797,8 @@ import Data.Collections ) as Collections -data Box value = - Box value +data Box value + = Box value | Many value value value longApplication ∷ diff --git a/tests-integration/fixtures/formatting/1791042120_alternate_source_wrapping/Main.snap b/tests-integration/fixtures/formatting/1791042120_alternate_source_wrapping/Main.snap index b187966fd..dc0241bca 100644 --- a/tests-integration/fixtures/formatting/1791042120_alternate_source_wrapping/Main.snap +++ b/tests-integration/fixtures/formatting/1791042120_alternate_source_wrapping/Main.snap @@ -18,8 +18,8 @@ import Data.Collections ) as Collections -data Box value = - Box value +data Box value + = Box value | Many value value value longApplication :: @@ -427,8 +427,8 @@ import Data.Collections ) as Collections -data Box value = - Box value +data Box value + = Box value | Many value value value longApplication :: @@ -600,8 +600,8 @@ import Data.Collections ) as Collections -data Box value = - Box value +data Box value + = Box value | Many value value value longApplication :: @@ -779,8 +779,8 @@ import Data.Collections ) as Collections -data Box value = - Box value +data Box value + = Box value | Many value value value longApplication ∷ diff --git a/tests-integration/fixtures/formatting/1791046620_declaration_separators/Main.snap b/tests-integration/fixtures/formatting/1791046620_declaration_separators/Main.snap index 4584a4519..07c3bb5d2 100644 --- a/tests-integration/fixtures/formatting/1791046620_declaration_separators/Main.snap +++ b/tests-integration/fixtures/formatting/1791046620_declaration_separators/Main.snap @@ -5,8 +5,8 @@ assertion_line: 127 === width 40, indent 2 === module Main where -data Command = - Initialize Configuration +data Command + = Initialize Configuration | Execute Request Environment | Shutdown Reason | Restart @@ -47,8 +47,8 @@ instance command :: ( Show configuration === width 80, indent 2 === module Main where -data Command = - Initialize Configuration +data Command + = Initialize Configuration | Execute Request Environment | Shutdown Reason | Restart @@ -78,8 +78,8 @@ instance command :: (Show configuration, Eq environment) => === width 40, indent 4, unicode === module Main where -data Command = - Initialize Configuration +data Command + = Initialize Configuration | Execute Request Environment | Shutdown Reason | Restart diff --git a/tests-integration/fixtures/formatting/1791048960_commented_separators/Main.snap b/tests-integration/fixtures/formatting/1791048960_commented_separators/Main.snap index 4c2d09f24..02b2ebd35 100644 --- a/tests-integration/fixtures/formatting/1791048960_commented_separators/Main.snap +++ b/tests-integration/fixtures/formatting/1791048960_commented_separators/Main.snap @@ -1,12 +1,12 @@ --- source: tests-integration/tests/formatting.rs -assertion_line: 118 +assertion_line: 127 --- === width 80, indent 2 === module Main where -data Choice = - First -- first constructor +data Choice + = First -- first constructor | Second data Another @@ -31,8 +31,8 @@ arrow :: === width 20, indent 2 === module Main where -data Choice = - First -- first constructor +data Choice + = First -- first constructor | Second data Another @@ -61,8 +61,8 @@ arrow :: === width 40, indent 4, unicode === module Main where -data Choice = - First -- first constructor +data Choice + = First -- first constructor | Second data Another diff --git a/tests-integration/fixtures/formatting/1791093360_leading_comma_layout_boundaries/Main.snap b/tests-integration/fixtures/formatting/1791093360_leading_comma_layout_boundaries/Main.snap index ac94b9b6c..93c2d8fd2 100644 --- a/tests-integration/fixtures/formatting/1791093360_leading_comma_layout_boundaries/Main.snap +++ b/tests-integration/fixtures/formatting/1791093360_leading_comma_layout_boundaries/Main.snap @@ -87,13 +87,13 @@ type Tail row = { | row } type RowTail row = (| row) -data Wrapped = - Wrapped - { firstField :: - FirstType - , secondField :: - SecondType - } +data Wrapped + = Wrapped + { firstField :: + FirstType + , secondField :: + SecondType + } literal = [ """first @@ -214,11 +214,11 @@ doLocal = do type Tail row = { | row } type RowTail row = (| row) -data Wrapped = - Wrapped - { firstField ∷ FirstType - , secondField ∷ SecondType - } +data Wrapped + = Wrapped + { firstField ∷ FirstType + , secondField ∷ SecondType + } literal = [ """first diff --git a/tests-integration/fixtures/formatting/1791361260_import_export_list_alignment/Main.snap b/tests-integration/fixtures/formatting/1791361260_import_export_list_alignment/Main.snap index 2ec187ea6..48e045d3e 100644 --- a/tests-integration/fixtures/formatting/1791361260_import_export_list_alignment/Main.snap +++ b/tests-integration/fixtures/formatting/1791361260_import_export_list_alignment/Main.snap @@ -95,6 +95,6 @@ import Library first = 1 second = 2 -data Box = - First +data Box + = First | Second From 2b787b9e438ef213725b3e118236a2aedf9034eb Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 7 Oct 2026 14:16:36 +0000 Subject: [PATCH 3/6] Break before delimited block expressions Place parentheses, arrays, records, and record updates containing do or ado blocks below their prefixes. Apply the same rule to bindings, arguments, and operator operands instead of letting block bodies drift rightward. Keep expression access and block detection on Tree, and include regenerated delimited-block and continuation-boundary fixtures. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-01a11570-3ec2-7539-bc19-3d4825503ee1 --- compiler-frontend/formatting/src/printer.rs | 47 +++-- .../1791044520_delimited_do_headers/Main.purs | 3 + .../1791044520_delimited_do_headers/Main.snap | 176 ++++++++++-------- .../Main.snap | 89 ++++----- 4 files changed, 185 insertions(+), 130 deletions(-) diff --git a/compiler-frontend/formatting/src/printer.rs b/compiler-frontend/formatting/src/printer.rs index 67b8a97d9..6f4c3e665 100644 --- a/compiler-frontend/formatting/src/printer.rs +++ b/compiler-frontend/formatting/src/printer.rs @@ -65,6 +65,17 @@ impl Tree { .collect() } + fn expression(&self) -> Option<&Tree> { + if self.kind == ExpressionTermArgument { self.children.first() } else { None } + } + + fn is_delimited_block(&self) -> bool { + matches!( + self.kind, + ExpressionParenthesized | ExpressionArray | ExpressionRecord | RecordUpdates + ) && self.has_inline_block() + } + fn ends_offside(&self) -> bool { if matches!( self.kind, @@ -79,12 +90,12 @@ impl Tree { self.elements().last().is_some_and(|child| child.ends_offside()) } - fn has_inline_do(&self) -> bool { + fn has_inline_block(&self) -> bool { match self.kind { ExpressionDo | ExpressionAdo => true, ExpressionLetIn | ExpressionIfThenElse | ExpressionCaseOf => false, - WhereExpression => self.children.first().is_some_and(Tree::has_inline_do), - _ => self.children.iter().any(Tree::has_inline_do), + WhereExpression => self.children.first().is_some_and(Tree::has_inline_block), + _ => self.children.iter().any(Tree::has_inline_block), } } } @@ -327,10 +338,11 @@ impl<'arena> Printer<'arena, '_> { }; let margin = context.margin + self.config.indent_width; let head = self.fixed(head, Context { following: Some(margin), ..context })?.group(); - if tail.iter().any(|tree| tree.has_inline_do()) { + if tail.iter().any(|tree| tree.has_inline_block()) { let inline = self.fixed(tail, context)?; let broken = self.fixed(tail, Context { margin, ..context })?; - return Ok(head.append(self.attach(first.start, Gap::Soft, inline, broken)?)); + let gap = if first.is_delimited_block() { Gap::Hard(1) } else { Gap::Soft }; + return Ok(head.append(self.attach(first.start, gap, inline, broken)?)); } let tail = self.fixed(tail, Context { margin, ..context })?; Ok(head @@ -397,7 +409,13 @@ impl<'arena> Printer<'arena, '_> { head_length, context, context.margin + self.config.indent_width, - |_, _, _| Gap::Soft, + |_, _, current| { + if current.expression().unwrap_or(current).is_delimited_block() { + Gap::Hard(1) + } else { + Gap::Soft + } + }, ) } @@ -447,7 +465,7 @@ impl<'arena> Printer<'arena, '_> { let fixed_gaps = (0..tail.len()).all(|position| !matches!(gap(position), Gap::Soft | Gap::SoftEmpty)); if fixed_gaps - || elements.iter().any(|tree| tree.has_inline_do()) + || elements.iter().any(|tree| tree.has_inline_block()) || head.last().is_some_and(|tree| tree.kind == ClassConstraints) { let mut inline_suffix = self.arena.nil(); @@ -617,7 +635,7 @@ impl<'arena> Printer<'arena, '_> { .nest(self.config.indent_width as isize); let operand_document = |operand: &Tree, context: Context| { let document = self.tree(operand, context)?; - let document = if operand.has_inline_do() { document } else { document.align() }; + let document = if operand.has_inline_block() { document } else { document.align() }; Ok::<_, FormatError>(document) }; let pair = |operator: &Tree, operand: &Tree, context: Context| { @@ -630,12 +648,14 @@ impl<'arena> Printer<'arena, '_> { let gap = |position: usize| { if elements[position - 1].ends_offside() { Gap::Hard(1) } else { Gap::Soft } }; - if elements.iter().any(|tree| tree.has_inline_do()) { + if elements.iter().any(|tree| tree.has_inline_block()) { let mut inline_suffix = self.arena.nil(); let mut broken_suffix = self.arena.nil(); for position in (1..elements.len()).step_by(2).rev() { let operator = elements[position]; let operand = elements[position + 1]; + let operand_gap = + if operand.is_delimited_block() { Gap::Hard(1) } else { Gap::Space }; let following = if position + 2 < elements.len() { Some(margin) } else { context.following }; let inline = operand_document(operand, Context { following, ..context })? @@ -644,10 +664,15 @@ impl<'arena> Printer<'arena, '_> { operand_document(operand, Context { margin, following })?.append(broken_suffix); let inline = self .tree(operator, Context { following: Some(margin), ..context })? - .append(self.attach(operand.start, Gap::Space, inline, broken.clone())?); + .append(self.attach(operand.start, operand_gap, inline, broken.clone())?); + let broken = self.boundary(operand.start, operand_gap)?.group().append(broken); + let broken = if operand.is_delimited_block() { + broken.nest(self.config.indent_width as isize) + } else { + broken + }; let broken = self .tree(operator, Context { margin, following: Some(margin) })? - .append(self.boundary(operand.start, Gap::Space)?.group()) .append(broken); inline_suffix = self.attach(operator.start, gap(position), inline, broken.clone())?; diff --git a/tests-integration/fixtures/formatting/1791044520_delimited_do_headers/Main.purs b/tests-integration/fixtures/formatting/1791044520_delimited_do_headers/Main.purs index cb52dc73c..0568b3cd5 100644 --- a/tests-integration/fixtures/formatting/1791044520_delimited_do_headers/Main.purs +++ b/tests-integration/fixtures/formatting/1791044520_delimited_do_headers/Main.purs @@ -23,3 +23,6 @@ applicative = use (ado wrapped = (combine firstArgument secondArgument) nested = ((combine firstArgument secondArgument)) + +operator = identity $ { onClick: do + action, other: 1 } diff --git a/tests-integration/fixtures/formatting/1791044520_delimited_do_headers/Main.snap b/tests-integration/fixtures/formatting/1791044520_delimited_do_headers/Main.snap index 60c6bdcd9..4749b9200 100644 --- a/tests-integration/fixtures/formatting/1791044520_delimited_do_headers/Main.snap +++ b/tests-integration/fixtures/formatting/1791044520_delimited_do_headers/Main.snap @@ -10,31 +10,30 @@ lambda = for_ values log value ) -parenthesized = use ( do - action - ) next - -record = { onClick: do - action - , other: 1 - } - -array = [ do - firstAction - , do - secondAction - ] - -applicative = use ( ado - first <- - firstAction - second <- - secondAction - in - combine - first - second - ) +parenthesized = use + ( do + action + ) next + +record = + { onClick: do + action + , other: 1 + } + +array = + [ do + firstAction + , do + secondAction + ] + +applicative = use + ( ado + first <- firstAction + second <- secondAction + in combine first second + ) wrapped = ( combine @@ -48,69 +47,86 @@ nested = ) ) +operator = identity $ + { onClick: do + action + , other: 1 + } + === width 80, indent 2 === module Main where -lambda = for_ values ( \value -> do - log value - ) - -parenthesized = use ( do - action - ) next - -record = { onClick: do - action - , other: 1 - } - -array = [ do - firstAction - , do - secondAction - ] +lambda = for_ values + ( \value -> do + log value + ) -applicative = use ( ado - first <- firstAction - second <- secondAction - in combine first second - ) +parenthesized = use + ( do + action + ) next + +record = + { onClick: do + action + , other: 1 + } + +array = + [ do + firstAction + , do + secondAction + ] + +applicative = use + ( ado + first <- firstAction + second <- secondAction + in combine first second + ) wrapped = (combine firstArgument secondArgument) nested = ((combine firstArgument secondArgument)) +operator = identity $ + { onClick: do + action + , other: 1 + } + === width 40, indent 4, unicode === module Main where -lambda = for_ values ( \value → do - log value - ) - -parenthesized = use ( do - action - ) next - -record = { onClick: do - action - , other: 1 - } - -array = [ do - firstAction - , do - secondAction - ] - -applicative = use ( ado - first ← - firstAction - second ← - secondAction - in - combine - first - second - ) +lambda = for_ values + ( \value → do + log value + ) + +parenthesized = use + ( do + action + ) next + +record = + { onClick: do + action + , other: 1 + } + +array = + [ do + firstAction + , do + secondAction + ] + +applicative = use + ( ado + first ← firstAction + second ← secondAction + in combine first second + ) wrapped = ( combine @@ -123,3 +139,9 @@ nested = secondArgument ) ) + +operator = identity $ + { onClick: do + action + , other: 1 + } diff --git a/tests-integration/fixtures/formatting/1791093360_leading_comma_layout_boundaries/Main.snap b/tests-integration/fixtures/formatting/1791093360_leading_comma_layout_boundaries/Main.snap index 93c2d8fd2..5eeb94219 100644 --- a/tests-integration/fixtures/formatting/1791093360_leading_comma_layout_boundaries/Main.snap +++ b/tests-integration/fixtures/formatting/1791093360_leading_comma_layout_boundaries/Main.snap @@ -34,11 +34,11 @@ effect = do } finalAction -argument = use [ do - action - , nextArgument - ] - finalArgument +argument = use + [ do + action + , nextArgument + ] finalArgument patterns first second = @@ -110,11 +110,12 @@ second""" ] updateEffect = - record { field = do - action - , otherField = - secondArgument - } + record + { field = do + action + , otherField = + secondArgument + } qualifiedUpdate = M.record { field = @@ -146,12 +147,12 @@ sectionUpdate = secondArgument } widePrefix = use - "界界界界" { field: - do - action - , otherField: - secondArgument - } + "界界界界" + { field: do + action + , otherField: + secondArgument + } === width 40, indent 4, unicode === module Main where @@ -179,10 +180,11 @@ effect = do } finalAction -argument = use [ do - action - , nextArgument - ] finalArgument +argument = use + [ do + action + , nextArgument + ] finalArgument patterns first second = case first, second of @@ -230,11 +232,11 @@ second""" } ] -updateEffect = record { field = do - action - , otherField = - secondArgument - } +updateEffect = record + { field = do + action + , otherField = secondArgument + } qualifiedUpdate = M.record { field = firstArgument @@ -255,11 +257,11 @@ sectionUpdate = { field = firstArgument , otherField = secondArgument } -widePrefix = use "界界界界" { field: do - action - , otherField: - secondArgument - } +widePrefix = use "界界界界" + { field: do + action + , otherField: secondArgument + } === width 80, indent 2 === module Main where @@ -284,10 +286,11 @@ effect = do } finalAction -argument = use [ do - action - , nextArgument - ] finalArgument +argument = use + [ do + action + , nextArgument + ] finalArgument patterns first second = case first, second of @@ -316,10 +319,11 @@ second""" , { nested: [ { value: firstArgument }, { value: secondArgument } ] } ] -updateEffect = record { field = do - action - , otherField = secondArgument - } +updateEffect = record + { field = do + action + , otherField = secondArgument + } qualifiedUpdate = M.record { field = firstArgument, otherField = secondArgument } accessUpdate = @@ -328,7 +332,8 @@ parenthesizedUpdate = (chooseRecord firstArgument) { field = firstArgument, otherField = secondArgument } sectionUpdate = _ { field = firstArgument, otherField = secondArgument } -widePrefix = use "界界界界" { field: do - action - , otherField: secondArgument - } +widePrefix = use "界界界界" + { field: do + action + , otherField: secondArgument + } From 342a5031b26640adeb99a1287156f046348a682b Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 7 Oct 2026 14:20:35 +0000 Subject: [PATCH 4/6] Keep case headers compact before splitting scrutinees Treat case expressions as inline blocks and measure complete headers independently of their branches. Keep a fitting header beside its prefix, move it intact when necessary, and split case and of only when the scrutinees require continuation layout. Cover width boundaries, multiple and nested scrutinees, comments, parentheses, operator operands, and nested block continuations. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-01a11570-3ec2-7539-bc19-3d4825503ee1 --- compiler-frontend/formatting/src/printer.rs | 138 +++++- .../Main.snap | 7 +- .../1791034641_width_aware/Main.snap | 62 +-- .../Main.snap | 62 +-- .../Main.purs | 17 + .../Main.snap | 147 ++++-- .../Main.snap | 21 +- .../1791047940_syntax_boundaries/Main.snap | 30 +- .../1791048960_nested_blank_lines/Main.snap | 16 +- .../Main.snap | 35 +- .../Main.snap | 25 +- .../1791093720_leading_comma_guards/Main.snap | 41 +- .../Main.snap | 52 +- .../Main.purs | 50 ++ .../Main.snap | 460 ++++++++++++++++++ 15 files changed, 917 insertions(+), 246 deletions(-) create mode 100644 tests-integration/fixtures/formatting/1791361260_case_header_attachment/Main.purs create mode 100644 tests-integration/fixtures/formatting/1791361260_case_header_attachment/Main.snap diff --git a/compiler-frontend/formatting/src/printer.rs b/compiler-frontend/formatting/src/printer.rs index 6f4c3e665..cb6d53888 100644 --- a/compiler-frontend/formatting/src/printer.rs +++ b/compiler-frontend/formatting/src/printer.rs @@ -92,8 +92,8 @@ impl Tree { fn has_inline_block(&self) -> bool { match self.kind { - ExpressionDo | ExpressionAdo => true, - ExpressionLetIn | ExpressionIfThenElse | ExpressionCaseOf => false, + ExpressionDo | ExpressionAdo | ExpressionCaseOf => true, + ExpressionLetIn | ExpressionIfThenElse => false, WhereExpression => self.children.first().is_some_and(Tree::has_inline_block), _ => self.children.iter().any(Tree::has_inline_block), } @@ -342,7 +342,13 @@ impl<'arena> Printer<'arena, '_> { let inline = self.fixed(tail, context)?; let broken = self.fixed(tail, Context { margin, ..context })?; let gap = if first.is_delimited_block() { Gap::Hard(1) } else { Gap::Soft }; - return Ok(head.append(self.attach(first.start, gap, inline, broken)?)); + return Ok(head.append(self.attach( + first.start, + gap, + inline, + broken, + self.case_header(first, context)?, + )?)); } let tail = self.fixed(tail, Context { margin, ..context })?; Ok(head @@ -361,6 +367,7 @@ impl<'arena> Printer<'arena, '_> { gap: Gap, inline: Doc<'arena>, broken: Doc<'arena>, + header: Option>, ) -> Result, FormatError> { let arena = self.arena; let indent = self.config.indent_width; @@ -372,6 +379,22 @@ impl<'arena> Printer<'arena, '_> { // comments aligned with the continuation they precede. return Ok(self.boundary(index, gap)?.append(broken).nest(indent as isize)); } + if let Some(header) = header { + // Probe the complete flat header, not the first line of its fallback. + // Branch lines and oversized branch atoms cannot affect attachment. + let probe = arena.fail().flat_alt(self.boundary(index, gap)?.append(header)).group(); + let inline = self.boundary(index, Gap::Space)?.append(inline); + let broken = self.boundary(index, Gap::Hard(1))?.append(broken).nest(indent as isize); + let width = self.config.line_width; + return Ok(arena.column(move |column| { + let mut rendered = String::new(); + if probe.render_fmt(width.saturating_sub(column), &mut rendered).is_ok() { + inline.clone().into_doc() + } else { + broken.clone().into_doc() + } + })); + } if matches!(gap, Gap::Tight | Gap::Space | Gap::BrokenSpace) { return Ok(self.boundary(index, gap)?.append(inline)); } @@ -483,7 +506,13 @@ impl<'arena> Printer<'arena, '_> { inline_suffix = inline; broken_suffix = broken; } else { - inline_suffix = self.attach(element.start, gap, inline, broken.clone())?; + inline_suffix = self.attach( + element.start, + gap, + inline, + broken.clone(), + self.case_header(element, context)?, + )?; broken_suffix = self.boundary(element.start, gap)?.group().append(broken); } } @@ -658,14 +687,45 @@ impl<'arena> Printer<'arena, '_> { if operand.is_delimited_block() { Gap::Hard(1) } else { Gap::Space }; let following = if position + 2 < elements.len() { Some(margin) } else { context.following }; + let operand_header = self.case_header(operand, context)?; + let operator_header = if let Some(header) = &operand_header { + Some( + self.tree(operator, context)? + .append(self.boundary(operand.start, Gap::Space)?) + .append(header.clone()), + ) + } else { + None + }; let inline = operand_document(operand, Context { following, ..context })? .append(inline_suffix); + let moved = if operand.kind == ExpressionCaseOf { + Some( + operand_document( + operand, + Context { margin: margin + self.config.indent_width, following }, + )? + .append(broken_suffix.clone()), + ) + } else { + None + }; let broken = operand_document(operand, Context { margin, following })?.append(broken_suffix); let inline = self .tree(operator, Context { following: Some(margin), ..context })? - .append(self.attach(operand.start, operand_gap, inline, broken.clone())?); - let broken = self.boundary(operand.start, operand_gap)?.group().append(broken); + .append(self.attach( + operand.start, + operand_gap, + inline, + broken.clone(), + operand_header.clone(), + )?); + let broken = if let Some(moved) = moved { + self.attach(operand.start, operand_gap, broken, moved, operand_header)? + } else { + self.boundary(operand.start, operand_gap)?.group().append(broken) + }; let broken = if operand.is_delimited_block() { broken.nest(self.config.indent_width as isize) } else { @@ -674,8 +734,13 @@ impl<'arena> Printer<'arena, '_> { let broken = self .tree(operator, Context { margin, following: Some(margin) })? .append(broken); - inline_suffix = - self.attach(operator.start, gap(position), inline, broken.clone())?; + inline_suffix = self.attach( + operator.start, + gap(position), + inline, + broken.clone(), + operator_header, + )?; broken_suffix = self.boundary(operator.start, gap(position))?.group().append(broken); } @@ -715,6 +780,62 @@ impl<'arena> Printer<'arena, '_> { Ok(document.group()) } + fn case_header( + &self, + tree: &Tree, + context: Context, + ) -> Result>, FormatError> { + let expression = tree.expression().unwrap_or(tree); + if expression.kind == ExpressionCaseOf { + self.fixed(&expression.elements()[..3], context).map(Some) + } else { + Ok(None) + } + } + + fn case_expression( + &self, + elements: &[&Tree], + context: Context, + ) -> Result, FormatError> { + let header = self.fixed(&elements[..3], context)?; + let margin = context.margin + self.config.indent_width; + let items = elements[1].elements(); + let mut trunk = self.arena.nil(); + for (position, item) in items.iter().enumerate() { + let gap = if item.kind == COMMA { Gap::SoftEmpty } else { self.fixed_gap(item.start) }; + let boundary = + if position == 0 { self.arena.nil() } else { self.boundary(item.start, gap)? }; + let indentation = if position > 0 && items[position - 1].kind == COMMA { 2 } else { 0 }; + let item_context = Context { margin: margin + indentation, following: None }; + trunk = trunk + .append(boundary.append(self.tree(item, item_context)?).nest(indentation as isize)); + } + let broken = self + .tree(elements[0], context)? + .append( + self.boundary(elements[1].start, Gap::Hard(1))? + .append(trunk.group()) + .nest(self.config.indent_width as isize), + ) + .append(self.boundary(elements[2].start, Gap::Hard(1))?) + .append(self.tree(elements[2], context)?); + let arena = self.arena; + let probe = arena.fail().flat_alt(header).group(); + let width = self.config.line_width; + // A comment after `of` belongs to the branch boundary, so it must not + // participate in the decision to split the header. + let header = arena.column(move |column| { + let mut rendered = String::new(); + if probe.render_fmt(width.saturating_sub(column), &mut rendered).is_ok() { + arena.text(rendered).into_doc() + } else { + broken.clone().into_doc() + } + }); + Ok(header.append(self.tree(elements[3], context)?)) + } + fn where_expression( &self, head: &[&Tree], @@ -872,6 +993,7 @@ impl<'arena> Printer<'arena, '_> { | InstanceConstraints | ClassConstraints => self.delimited(tree, context, None), ExpressionIfThenElse => self.conditional(&elements, context), + ExpressionCaseOf => self.case_expression(&elements, context), ValueEquation | LetBindingEquation | LetBindingPattern diff --git a/tests-integration/fixtures/formatting/1791030060_layout_and_punctuation/Main.snap b/tests-integration/fixtures/formatting/1791030060_layout_and_punctuation/Main.snap index 906e1feb2..034e02d9e 100644 --- a/tests-integration/fixtures/formatting/1791030060_layout_and_punctuation/Main.snap +++ b/tests-integration/fixtures/formatting/1791030060_layout_and_punctuation/Main.snap @@ -1,6 +1,6 @@ --- source: tests-integration/tests/formatting.rs -assertion_line: 124 +assertion_line: 127 --- module Main (test, Box(..)) where @@ -37,9 +37,8 @@ local x = in y -branch x = - case x of - Box y -> if true then y else 0 +branch x = case x of + Box y -> if true then y else 0 record = { outer: { left: 1, right: 2 }, tail: 3 } diff --git a/tests-integration/fixtures/formatting/1791034641_width_aware/Main.snap b/tests-integration/fixtures/formatting/1791034641_width_aware/Main.snap index f5ea844c3..5e1067108 100644 --- a/tests-integration/fixtures/formatting/1791034641_width_aware/Main.snap +++ b/tests-integration/fixtures/formatting/1791034641_width_aware/Main.snap @@ -124,12 +124,11 @@ commentedOperatorDo = do -- before an inline block + do action -operatorPattern value = - case value of - firstElement - : secondElement - : remainingElements -> - transform remainingElements +operatorPattern value = case value of + firstElement + : secondElement + : remainingElements -> + transform remainingElements class Base value <= Derived value builtinBind = do @@ -264,10 +263,9 @@ commentedOperatorDo = do -- before an inline block + do action -operatorPattern value = - case value of - firstElement : secondElement : remainingElements -> - transform remainingElements +operatorPattern value = case value of + firstElement : secondElement : remainingElements -> + transform remainingElements class Base value <= Derived value builtinBind = do @@ -373,9 +371,8 @@ commentedOperatorDo = do -- before an inline block + do action -operatorPattern value = - case value of - firstElement : secondElement : remainingElements -> transform remainingElements +operatorPattern value = case value of + firstElement : secondElement : remainingElements -> transform remainingElements class Base value <= Derived value builtinBind = do @@ -542,12 +539,11 @@ commentedOperatorDo = do -- before an inline block + do action -operatorPattern value = - case value of - firstElement - : secondElement - : remainingElements -> - transform remainingElements +operatorPattern value = case value of + firstElement + : secondElement + : remainingElements -> + transform remainingElements class Base value <= Derived value builtinBind = do @@ -723,12 +719,11 @@ commentedOperatorDo = do -- before an inline block + do action -operatorPattern value = - case value of - firstElement - : secondElement - : remainingElements -> - transform remainingElements +operatorPattern value = case value of + firstElement + : secondElement + : remainingElements -> + transform remainingElements class Base value <= Derived value builtinBind = do @@ -903,12 +898,11 @@ commentedOperatorDo = do -- before an inline block + do action -operatorPattern value = - case value of - firstElement - : secondElement - : remainingElements → - transform remainingElements +operatorPattern value = case value of + firstElement + : secondElement + : remainingElements → + transform remainingElements class Base value ⇐ Derived value builtinBind = do @@ -1041,10 +1035,8 @@ commentedOperatorDo = do -- before an inline block + do action -operatorPattern value = - case value of - firstElement : secondElement : remainingElements → - transform remainingElements +operatorPattern value = case value of + firstElement : secondElement : remainingElements → transform remainingElements class Base value ⇐ Derived value builtinBind = do diff --git a/tests-integration/fixtures/formatting/1791042120_alternate_source_wrapping/Main.snap b/tests-integration/fixtures/formatting/1791042120_alternate_source_wrapping/Main.snap index dc0241bca..434d74983 100644 --- a/tests-integration/fixtures/formatting/1791042120_alternate_source_wrapping/Main.snap +++ b/tests-integration/fixtures/formatting/1791042120_alternate_source_wrapping/Main.snap @@ -120,12 +120,11 @@ commentedOperatorDo = do -- before an inline block + do action -operatorPattern value = - case value of - firstElement - : secondElement - : remainingElements -> - transform remainingElements +operatorPattern value = case value of + firstElement + : secondElement + : remainingElements -> + transform remainingElements class Base value <= Derived value builtinBind = do @@ -257,10 +256,9 @@ commentedOperatorDo = do -- before an inline block + do action -operatorPattern value = - case value of - firstElement : secondElement : remainingElements -> - transform remainingElements +operatorPattern value = case value of + firstElement : secondElement : remainingElements -> + transform remainingElements class Base value <= Derived value builtinBind = do @@ -363,9 +361,8 @@ commentedOperatorDo = do -- before an inline block + do action -operatorPattern value = - case value of - firstElement : secondElement : remainingElements -> transform remainingElements +operatorPattern value = case value of + firstElement : secondElement : remainingElements -> transform remainingElements class Base value <= Derived value builtinBind = do @@ -528,12 +525,11 @@ commentedOperatorDo = do -- before an inline block + do action -operatorPattern value = - case value of - firstElement - : secondElement - : remainingElements -> - transform remainingElements +operatorPattern value = case value of + firstElement + : secondElement + : remainingElements -> + transform remainingElements class Base value <= Derived value builtinBind = do @@ -705,12 +701,11 @@ commentedOperatorDo = do -- before an inline block + do action -operatorPattern value = - case value of - firstElement - : secondElement - : remainingElements -> - transform remainingElements +operatorPattern value = case value of + firstElement + : secondElement + : remainingElements -> + transform remainingElements class Base value <= Derived value builtinBind = do @@ -881,12 +876,11 @@ commentedOperatorDo = do -- before an inline block + do action -operatorPattern value = - case value of - firstElement - : secondElement - : remainingElements → - transform remainingElements +operatorPattern value = case value of + firstElement + : secondElement + : remainingElements → + transform remainingElements class Base value ⇐ Derived value builtinBind = do @@ -1016,10 +1010,8 @@ commentedOperatorDo = do -- before an inline block + do action -operatorPattern value = - case value of - firstElement : secondElement : remainingElements → - transform remainingElements +operatorPattern value = case value of + firstElement : secondElement : remainingElements → transform remainingElements class Base value ⇐ Derived value builtinBind = do diff --git a/tests-integration/fixtures/formatting/1791044520_nested_do_continuations/Main.purs b/tests-integration/fixtures/formatting/1791044520_nested_do_continuations/Main.purs index 59b830dd9..194f176bf 100644 --- a/tests-integration/fixtures/formatting/1791044520_nested_do_continuations/Main.purs +++ b/tests-integration/fixtures/formatting/1791044520_nested_do_continuations/Main.purs @@ -33,3 +33,20 @@ empty = ado emptyLine = ado -- before the result in 2 + +letCase = let y = 1 in case y of + value -> value + +conditionalCase = + if condition + then case value of + A -> first + else do + fallback + +lambdaCase = \value -> case value of + A -> do + firstAction + B -> ado + result <- secondAction + in result diff --git a/tests-integration/fixtures/formatting/1791044520_nested_do_continuations/Main.snap b/tests-integration/fixtures/formatting/1791044520_nested_do_continuations/Main.snap index dda2f0afd..b57773fab 100644 --- a/tests-integration/fixtures/formatting/1791044520_nested_do_continuations/Main.snap +++ b/tests-integration/fixtures/formatting/1791044520_nested_do_continuations/Main.snap @@ -1,6 +1,6 @@ --- source: tests-integration/tests/formatting.rs -assertion_line: 92 +assertion_line: 127 --- === width 30, indent 2 === module Main where @@ -21,20 +21,19 @@ statement = do action y pure x -branches value = - case value of - A -> - let - y = 1 - in - do - action y - B -> - if condition - then do - firstAction - secondAction - else fallback +branches value = case value of + A -> + let + y = 1 + in + do + action y + B -> + if condition + then do + firstAction + secondAction + else fallback conditional = if condition @@ -57,6 +56,28 @@ emptyLine = ado -- before the result in 2 +letCase = + let + y = 1 + in + case y of + value -> value + +conditionalCase = + if condition + then case value of + A -> first + else do + fallback + +lambdaCase = \value -> + case value of + A -> do + firstAction + B -> ado + result <- secondAction + in result + === width 80, indent 2 === module Main where @@ -76,20 +97,19 @@ statement = do action y pure x -branches value = - case value of - A -> - let - y = 1 - in - do - action y - B -> - if condition - then do - firstAction - secondAction - else fallback +branches value = case value of + A -> + let + y = 1 + in + do + action y + B -> + if condition + then do + firstAction + secondAction + else fallback conditional = if condition @@ -112,6 +132,27 @@ emptyLine = ado -- before the result in 2 +letCase = + let + y = 1 + in + case y of + value -> value + +conditionalCase = + if condition + then case value of + A -> first + else do + fallback + +lambdaCase = \value -> case value of + A -> do + firstAction + B -> ado + result <- secondAction + in result + === width 40, indent 4, unicode === module Main where @@ -131,20 +172,19 @@ statement = do action y pure x -branches value = - case value of - A → - let - y = 1 - in - do - action y - B → - if condition - then do - firstAction - secondAction - else fallback +branches value = case value of + A → + let + y = 1 + in + do + action y + B → + if condition + then do + firstAction + secondAction + else fallback conditional = if condition @@ -166,3 +206,24 @@ empty = ado emptyLine = ado -- before the result in 2 + +letCase = + let + y = 1 + in + case y of + value → value + +conditionalCase = + if condition + then case value of + A → first + else do + fallback + +lambdaCase = \value → case value of + A → do + firstAction + B → ado + result ← secondAction + in result diff --git a/tests-integration/fixtures/formatting/1791046620_tight_syntax_at_line_starts/Main.snap b/tests-integration/fixtures/formatting/1791046620_tight_syntax_at_line_starts/Main.snap index 25f9def97..b8f603cc7 100644 --- a/tests-integration/fixtures/formatting/1791046620_tight_syntax_at_line_starts/Main.snap +++ b/tests-integration/fixtures/formatting/1791046620_tight_syntax_at_line_starts/Main.snap @@ -30,10 +30,9 @@ terms = do ) ) -patterns value = - case value of - [ -1 ] -> 0 - _ -> 1 +patterns value = case value of + [ -1 ] -> 0 + _ -> 1 rows :: forall r. @@ -60,10 +59,9 @@ terms = do ) ) -patterns value = - case value of - [ -1 ] -> 0 - _ -> 1 +patterns value = case value of + [ -1 ] -> 0 + _ -> 1 rows :: forall r. { | r } -> { | r } -> Int quantified :: (forall b. b -> b) -> Int @@ -96,10 +94,9 @@ terms = do ) ) -patterns value = - case value of - [ -1 ] → 0 - _ → 1 +patterns value = case value of + [ -1 ] → 0 + _ → 1 rows ∷ ∀ r. diff --git a/tests-integration/fixtures/formatting/1791047940_syntax_boundaries/Main.snap b/tests-integration/fixtures/formatting/1791047940_syntax_boundaries/Main.snap index d796e2e20..e3f061020 100644 --- a/tests-integration/fixtures/formatting/1791047940_syntax_boundaries/Main.snap +++ b/tests-integration/fixtures/formatting/1791047940_syntax_boundaries/Main.snap @@ -60,12 +60,10 @@ guarded value | Just result <- lookup value, result > 0 = result conditional = if condition then 1 else 2 -caseResult = - case value of - Just result -> result -multipleCase = - case first, second of - Just result, Nothing -> result +caseResult = case value of + Just result -> result +multipleCase = case first, second of + Just result, Nothing -> result lambda = \value -> value parenthesized = (1) negative = -1 @@ -160,12 +158,10 @@ patternLocal = guarded value | Just result <- lookup value, result > 0 = result conditional = if condition then 1 else 2 -caseResult = - case value of - Just result -> result -multipleCase = - case first, second of - Just result, Nothing -> result +caseResult = case value of + Just result -> result +multipleCase = case first, second of + Just result, Nothing -> result lambda = \value -> value parenthesized = (1) negative = -1 @@ -256,12 +252,10 @@ guarded value | Just result ← lookup value, result > 0 = result conditional = if condition then 1 else 2 -caseResult = - case value of - Just result → result -multipleCase = - case first, second of - Just result, Nothing → result +caseResult = case value of + Just result → result +multipleCase = case first, second of + Just result, Nothing → result lambda = \value → value parenthesized = (1) negative = -1 diff --git a/tests-integration/fixtures/formatting/1791048960_nested_blank_lines/Main.snap b/tests-integration/fixtures/formatting/1791048960_nested_blank_lines/Main.snap index 8f95b8d1d..2f534b2c0 100644 --- a/tests-integration/fixtures/formatting/1791048960_nested_blank_lines/Main.snap +++ b/tests-integration/fixtures/formatting/1791048960_nested_blank_lines/Main.snap @@ -1,6 +1,6 @@ --- source: tests-integration/tests/formatting.rs -assertion_line: 114 +assertion_line: 127 --- === width 40, indent 2 === module Main where @@ -10,11 +10,10 @@ doValue = do second -caseValue = - case value of - First -> 1 +caseValue = case value of + First -> 1 - Second -> 2 + Second -> 2 whereValue = first where @@ -58,11 +57,10 @@ doValue = do second -caseValue = - case value of - First -> 1 +caseValue = case value of + First -> 1 - Second -> 2 + Second -> 2 whereValue = first where diff --git a/tests-integration/fixtures/formatting/1791093000_aligned_where_bindings/Main.snap b/tests-integration/fixtures/formatting/1791093000_aligned_where_bindings/Main.snap index b56383464..18ef61503 100644 --- a/tests-integration/fixtures/formatting/1791093000_aligned_where_bindings/Main.snap +++ b/tests-integration/fixtures/formatting/1791093000_aligned_where_bindings/Main.snap @@ -1,6 +1,6 @@ --- source: tests-integration/tests/formatting.rs -assertion_line: 118 +assertion_line: 127 --- === width 24, indent 1 === module Main where @@ -85,12 +85,11 @@ effect = do pure 1 sibling = 2 -branch input = - case input of - Just value -> result - where - result = value - Nothing -> fallback +branch input = case input of + Just value -> result + where + result = value + Nothing -> fallback where fallback = 0 @@ -146,12 +145,11 @@ effect = do pure 1 sibling = 2 -branch input = - case input of - Just value → result - where - result = value - Nothing → fallback +branch input = case input of + Just value → result + where + result = value + Nothing → fallback where fallback = 0 @@ -207,12 +205,11 @@ effect = do pure 1 sibling = 2 -branch input = - case input of - Just value -> result - where - result = value - Nothing -> fallback +branch input = case input of + Just value -> result + where + result = value + Nothing -> fallback where fallback = 0 diff --git a/tests-integration/fixtures/formatting/1791093360_leading_comma_layout_boundaries/Main.snap b/tests-integration/fixtures/formatting/1791093360_leading_comma_layout_boundaries/Main.snap index 5eeb94219..871af97ca 100644 --- a/tests-integration/fixtures/formatting/1791093360_leading_comma_layout_boundaries/Main.snap +++ b/tests-integration/fixtures/formatting/1791093360_leading_comma_layout_boundaries/Main.snap @@ -42,7 +42,9 @@ argument = use patterns first second = - case first, second of + case + first, second + of { firstField: value , secondField: @@ -164,10 +166,9 @@ effect = do value ← secondAction in value ] - { result: - case input of - Just value → value - Nothing → fallback + { result: case input of + Just value → value + Nothing → fallback , local: let value = firstArgument @@ -273,10 +274,9 @@ effect = do value <- secondAction in value ] - { result: - case input of - Just value -> value - Nothing -> fallback + { result: case input of + Just value -> value + Nothing -> fallback , local: let value = firstArgument @@ -292,10 +292,9 @@ argument = use , nextArgument ] finalArgument -patterns first second = - case first, second of - { firstField: value, secondField: other }, [ item, rest ] -> value - _, _ -> fallback +patterns first second = case first, second of + { firstField: value, secondField: other }, [ item, rest ] -> value + _, _ -> fallback named whole@{ firstField: value, secondField: other } = whole lambda = \{ firstField: value, secondField: other } -> value diff --git a/tests-integration/fixtures/formatting/1791093720_leading_comma_guards/Main.snap b/tests-integration/fixtures/formatting/1791093720_leading_comma_guards/Main.snap index fddb9a08c..d2a88b285 100644 --- a/tests-integration/fixtures/formatting/1791093720_leading_comma_guards/Main.snap +++ b/tests-integration/fixtures/formatting/1791093720_leading_comma_guards/Main.snap @@ -1,6 +1,6 @@ --- source: tests-integration/tests/formatting.rs -assertion_line: 118 +assertion_line: 127 --- === width 20, indent 2 === module Main where @@ -54,14 +54,13 @@ recordGuard value predicate value = 1 | otherwise = 2 -branch value = - case value of - result - | [ firstElementName - , secondElementName - ] <- result, - predicate result -> 1 - _ -> 2 +branch value = case value of + result + | [ firstElementName + , secondElementName + ] <- result, + predicate result -> 1 + _ -> 2 === width 40, indent 4, unicode === module Main where @@ -82,14 +81,13 @@ recordGuard value predicate value = 1 | otherwise = 2 -branch value = - case value of - result - | [ firstElementName - , secondElementName - ] ← result, - predicate result → 1 - _ → 2 +branch value = case value of + result + | [ firstElementName + , secondElementName + ] ← result, + predicate result → 1 + _ → 2 === width 120, indent 2 === module Main where @@ -102,8 +100,7 @@ recordGuard value | { firstFieldName: firstArgument, secondFieldName: secondArgument } == value, predicate value = 1 | otherwise = 2 -branch value = - case value of - result - | [ firstElementName, secondElementName ] <- result, predicate result -> 1 - _ -> 2 +branch value = case value of + result + | [ firstElementName, secondElementName ] <- result, predicate result -> 1 + _ -> 2 diff --git a/tests-integration/fixtures/formatting/1791134340_leading_operator_operands/Main.snap b/tests-integration/fixtures/formatting/1791134340_leading_operator_operands/Main.snap index 23ca2eb88..35bfd4ae8 100644 --- a/tests-integration/fixtures/formatting/1791134340_leading_operator_operands/Main.snap +++ b/tests-integration/fixtures/formatting/1791134340_leading_operator_operands/Main.snap @@ -22,11 +22,12 @@ localBinding = + anotherLongOperand branching = firstLongOperand - <=> case choice of - First -> - firstBranchValue - Second -> - secondBranchValue + <=> + case choice of + First -> + firstBranchValue + Second -> + secondBranchValue guarded value | firstLongCondition @@ -99,21 +100,19 @@ localBinding = bound = longBindingValue in bound + anotherLongOperand -branching = - firstLongOperand - <=> case choice of - First -> firstBranchValue - Second -> secondBranchValue +branching = firstLongOperand + <=> case choice of + First -> firstBranchValue + Second -> secondBranchValue guarded value | firstLongCondition value && secondLongCondition value = 1 | otherwise = 2 -pattern value = - case value of - Cons firstElement secondElement - : remainingElements -> result +pattern value = case value of + Cons firstElement secondElement + : remainingElements -> result typed :: forall value. @@ -173,10 +172,10 @@ localBinding = branching = firstLongOperand <=> case choice of - First -> - firstBranchValue - Second -> - secondBranchValue + First -> + firstBranchValue + Second -> + secondBranchValue guarded value | firstLongCondition @@ -250,22 +249,19 @@ localBinding = in bound + anotherLongOperand -branching = - firstLongOperand - <=> case choice of - First → firstBranchValue - Second → - secondBranchValue +branching = firstLongOperand + <=> case choice of + First → firstBranchValue + Second → secondBranchValue guarded value | firstLongCondition value && secondLongCondition value = 1 | otherwise = 2 -pattern value = - case value of - Cons firstElement secondElement - : remainingElements → result +pattern value = case value of + Cons firstElement secondElement + : remainingElements → result typed ∷ ∀ value. diff --git a/tests-integration/fixtures/formatting/1791361260_case_header_attachment/Main.purs b/tests-integration/fixtures/formatting/1791361260_case_header_attachment/Main.purs new file mode 100644 index 000000000..d7bb3dfc3 --- /dev/null +++ b/tests-integration/fixtures/formatting/1791361260_case_header_attachment/Main.purs @@ -0,0 +1,50 @@ +-- @format width=14 +-- @format width=15 +-- @format width=40 +-- @format width=50 +-- @format width=80 +-- @format width=40 indent=4 +module Main where + +abc = case _ of + 0 -> 1 + value -> value + +longHeader firstArgument secondArgument = case firstArgument of + 0 -> secondArgument + value -> case secondArgument of + 0 -> value + other -> other + +commented = -- keep the comment before the case + case _ of + value -> value + +handler = \value -> case value of + other -> other + +applied = consume case input of + value -> value + nextArgument + +scrutinee = case combine firstArgument secondArgument of + value -> "this indivisible body literal must not force the case header onto a new line" + +multiple = case combine firstArgument secondArgument, choose thirdArgument fourthArgument of + first, second -> first + +nestedScrutinee = case do + firstAction + secondAction + of + value -> value + +headerComment = case input -- scrutinee explanation + of + value -> value + +branchComment = case input of -- branch explanation + value -> value + +parenthesized = consume (case combine firstArgument secondArgument of + value -> value) diff --git a/tests-integration/fixtures/formatting/1791361260_case_header_attachment/Main.snap b/tests-integration/fixtures/formatting/1791361260_case_header_attachment/Main.snap new file mode 100644 index 000000000..5d4ca8f50 --- /dev/null +++ b/tests-integration/fixtures/formatting/1791361260_case_header_attachment/Main.snap @@ -0,0 +1,460 @@ +--- +source: tests-integration/tests/formatting.rs +assertion_line: 127 +--- +=== width 14, indent 2 === +module Main + where + +abc = + case _ of + 0 -> 1 + value -> + value + +longHeader firstArgument + secondArgument = + case + firstArgument + of + 0 -> + secondArgument + value -> + case + secondArgument + of + 0 -> + value + other -> + other + +commented = -- keep the comment before the case + case _ of + value -> + value + +handler = + \value -> + case + value + of + other -> + other + +applied = + consume + case + input + of + value -> + value + nextArgument + +scrutinee = + case + combine + firstArgument + secondArgument + of + value -> + "this indivisible body literal must not force the case header onto a new line" + +multiple = + case + combine + firstArgument + secondArgument + , choose + thirdArgument + fourthArgument + of + first, second -> + first + +nestedScrutinee = + case + do + firstAction + secondAction + of + value -> + value + +headerComment = + case + input -- scrutinee explanation + of + value -> + value + +branchComment = + case + input + of -- branch explanation + value -> + value + +parenthesized = + consume + ( case + combine + firstArgument + secondArgument + of + value -> + value + ) + +=== width 15, indent 2 === +module Main + where + +abc = case _ of + 0 -> 1 + value -> + value + +longHeader firstArgument + secondArgument = + case + firstArgument + of + 0 -> + secondArgument + value -> + case + secondArgument + of + 0 -> + value + other -> + other + +commented = -- keep the comment before the case + case _ of + value -> + value + +handler = + \value -> + case + value + of + other -> + other + +applied = + consume + case + input + of + value -> + value + nextArgument + +scrutinee = + case + combine + firstArgument + secondArgument + of + value -> + "this indivisible body literal must not force the case header onto a new line" + +multiple = + case + combine + firstArgument + secondArgument + , choose + thirdArgument + fourthArgument + of + first, second -> + first + +nestedScrutinee = + case + do + firstAction + secondAction + of + value -> + value + +headerComment = + case + input -- scrutinee explanation + of + value -> + value + +branchComment = + case input of -- branch explanation + value -> + value + +parenthesized = + consume + ( case + combine + firstArgument + secondArgument + of + value -> + value + ) + +=== width 40, indent 2 === +module Main where + +abc = case _ of + 0 -> 1 + value -> value + +longHeader firstArgument + secondArgument = case firstArgument of + 0 -> secondArgument + value -> case secondArgument of + 0 -> value + other -> other + +commented = -- keep the comment before the case + case _ of + value -> value + +handler = \value -> case value of + other -> other + +applied = consume case input of + value -> value + nextArgument + +scrutinee = + case + combine firstArgument secondArgument + of + value -> + "this indivisible body literal must not force the case header onto a new line" + +multiple = + case + combine firstArgument secondArgument + , choose + thirdArgument + fourthArgument + of + first, second -> first + +nestedScrutinee = + case + do + firstAction + secondAction + of + value -> value + +headerComment = + case + input -- scrutinee explanation + of + value -> value + +branchComment = case input of -- branch explanation + value -> value + +parenthesized = consume + ( case + combine + firstArgument + secondArgument + of + value -> value + ) + +=== width 50, indent 2 === +module Main where + +abc = case _ of + 0 -> 1 + value -> value + +longHeader firstArgument secondArgument = + case firstArgument of + 0 -> secondArgument + value -> case secondArgument of + 0 -> value + other -> other + +commented = -- keep the comment before the case + case _ of + value -> value + +handler = \value -> case value of + other -> other + +applied = consume case input of + value -> value + nextArgument + +scrutinee = + case combine firstArgument secondArgument of + value -> + "this indivisible body literal must not force the case header onto a new line" + +multiple = + case + combine firstArgument secondArgument + , choose thirdArgument fourthArgument + of + first, second -> first + +nestedScrutinee = + case + do + firstAction + secondAction + of + value -> value + +headerComment = + case + input -- scrutinee explanation + of + value -> value + +branchComment = case input of -- branch explanation + value -> value + +parenthesized = consume + ( case combine firstArgument secondArgument of + value -> value + ) + +=== width 80, indent 2 === +module Main where + +abc = case _ of + 0 -> 1 + value -> value + +longHeader firstArgument secondArgument = case firstArgument of + 0 -> secondArgument + value -> case secondArgument of + 0 -> value + other -> other + +commented = -- keep the comment before the case + case _ of + value -> value + +handler = \value -> case value of + other -> other + +applied = consume case input of + value -> value + nextArgument + +scrutinee = case combine firstArgument secondArgument of + value -> + "this indivisible body literal must not force the case header onto a new line" + +multiple = + case + combine firstArgument secondArgument, choose thirdArgument fourthArgument + of + first, second -> first + +nestedScrutinee = + case + do + firstAction + secondAction + of + value -> value + +headerComment = + case + input -- scrutinee explanation + of + value -> value + +branchComment = case input of -- branch explanation + value -> value + +parenthesized = consume + ( case combine firstArgument secondArgument of + value -> value + ) + +=== width 40, indent 4 === +module Main where + +abc = case _ of + 0 -> 1 + value -> value + +longHeader firstArgument + secondArgument = + case firstArgument of + 0 -> secondArgument + value -> case secondArgument of + 0 -> value + other -> other + +commented = -- keep the comment before the case + case _ of + value -> value + +handler = \value -> case value of + other -> other + +applied = consume case input of + value -> value + nextArgument + +scrutinee = + case + combine + firstArgument + secondArgument + of + value -> + "this indivisible body literal must not force the case header onto a new line" + +multiple = + case + combine + firstArgument + secondArgument + , choose + thirdArgument + fourthArgument + of + first, second -> first + +nestedScrutinee = + case + do + firstAction + secondAction + of + value -> value + +headerComment = + case + input -- scrutinee explanation + of + value -> value + +branchComment = case input of -- branch explanation + value -> value + +parenthesized = consume + ( case + combine + firstArgument + secondArgument + of + value -> value + ) From 597f67e33efe4286ee116bdb169b0a6c4a2e0988 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 7 Oct 2026 23:54:51 +0000 Subject: [PATCH 5/6] Flatten direct else-if chains into one layout group Keep clauses at one margin and decide compact versus multiline layout for the complete chain. Reuse continuation formatting for wrapped conditions, branch expressions, and blocks. Preserve parentheses and line-forcing comment boundaries, and retain nested then conditionals. Destructure validated conditional syntax with slice patterns. Cover compact and broken chains, wrapped conditions, block branches, comment boundaries, nested conditionals, parentheses, and operator operands. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-01a11570-3ec2-7539-bc19-3d4825503ee1 --- compiler-frontend/formatting/src/printer.rs | 48 ++- .../1791417240_else_if_chains/Main.purs | 33 +++ .../1791417240_else_if_chains/Main.snap | 278 ++++++++++++++++++ 3 files changed, 353 insertions(+), 6 deletions(-) create mode 100644 tests-integration/fixtures/formatting/1791417240_else_if_chains/Main.purs create mode 100644 tests-integration/fixtures/formatting/1791417240_else_if_chains/Main.snap diff --git a/compiler-frontend/formatting/src/printer.rs b/compiler-frontend/formatting/src/printer.rs index cb6d53888..f3f7cf415 100644 --- a/compiler-frontend/formatting/src/printer.rs +++ b/compiler-frontend/formatting/src/printer.rs @@ -767,17 +767,53 @@ impl<'arena> Printer<'arena, '_> { context: Context, ) -> Result, FormatError> { let mut document = self.arena.nil(); - for clause in elements.chunks(2) { - if clause[0].kind != IF { - document = document.append(self.boundary(clause[0].start, Gap::Soft)?); + let mut clauses = elements.to_vec(); + let mut leading_else: Option<&Tree> = None; + loop { + let [if_keyword, condition, then_keyword, consequent, else_keyword, alternative] = + clauses[..] + else { + unreachable!("invariant violated: validated conditional has six elements"); + }; + let head = if let Some(keyword) = leading_else.take() { + document = document.append(self.boundary(keyword.start, Gap::Soft)?); + vec![keyword, if_keyword] + } else { + vec![if_keyword] + }; + document = document.append(self.continuation( + &head, + &[condition], + Context { following: None, ..context }, + )?); + document = document.append(self.boundary(then_keyword.start, Gap::Soft)?).append( + self.continuation( + &[then_keyword], + &[consequent], + Context { following: None, ..context }, + )?, + ); + // Only direct alternatives share a layout; parentheses and + // line-forcing comments retain their continuation boundary. + if let [nested] = alternative.elements().as_slice() + && nested.kind == ExpressionIfThenElse + { + let (comments, _) = + trivia(self.lexed.annotation(nested.start).unwrap_or_default())?; + if !comments.iter().any(|comment| comment.forces_line()) { + leading_else = Some(else_keyword); + clauses = nested.elements(); + continue; + } } + document = document.append(self.boundary(else_keyword.start, Gap::Soft)?); document = document.append(self.continuation( - &clause[..1], - &clause[1..], + &[else_keyword], + &[alternative], Context { following: None, ..context }, )?); + return Ok(document.group()); } - Ok(document.group()) } fn case_header( diff --git a/tests-integration/fixtures/formatting/1791417240_else_if_chains/Main.purs b/tests-integration/fixtures/formatting/1791417240_else_if_chains/Main.purs new file mode 100644 index 000000000..f333daa99 --- /dev/null +++ b/tests-integration/fixtures/formatting/1791417240_else_if_chains/Main.purs @@ -0,0 +1,33 @@ +-- @format width=30 +-- @format width=80 +-- @format width=160 +-- @format width=40 indent=4 unicode=true +module Main where + +choose = if firstCondition then firstResult else if secondCondition then secondResult else if thirdCondition then thirdResult else fallback + +short = if a then x else if b then y else z + +blocks = if firstCondition then do + firstAction + secondAction +else if secondCondition then do + thirdAction +else fallback + +commented = if firstCondition then firstResult else -- next condition + if secondCondition then secondResult else fallback + +parenthesized = if firstCondition then firstResult else (if secondCondition then secondResult else fallback) + +nested = if firstCondition then if innerCondition then innerResult else innerFallback else if secondCondition then secondResult else fallback + +wrapped = if a then x else if predicate firstArgument secondArgument then y else z + +inlineComment = if a then x else {- next condition -} if b then y else z + +standaloneComment = if a then x else + {- next condition -} + if b then y else z + +operand = firstOperand + if firstCondition then firstResult else if secondCondition then secondResult else fallback diff --git a/tests-integration/fixtures/formatting/1791417240_else_if_chains/Main.snap b/tests-integration/fixtures/formatting/1791417240_else_if_chains/Main.snap new file mode 100644 index 000000000..574d8ac82 --- /dev/null +++ b/tests-integration/fixtures/formatting/1791417240_else_if_chains/Main.snap @@ -0,0 +1,278 @@ +--- +source: tests-integration/tests/formatting.rs +assertion_line: 127 +--- +=== width 30, indent 2 === +module Main where + +choose = + if firstCondition + then firstResult + else if secondCondition + then secondResult + else if thirdCondition + then thirdResult + else fallback + +short = + if a + then x + else if b + then y + else z + +blocks = + if firstCondition + then do + firstAction + secondAction + else if secondCondition + then do + thirdAction + else fallback + +commented = + if firstCondition + then firstResult + else -- next condition + if secondCondition + then secondResult + else fallback + +parenthesized = + if firstCondition + then firstResult + else + ( if secondCondition + then secondResult + else fallback + ) + +nested = + if firstCondition + then + if innerCondition + then innerResult + else innerFallback + else if secondCondition + then secondResult + else fallback + +wrapped = + if a + then x + else if + predicate + firstArgument + secondArgument + then y + else z + +inlineComment = + if a + then x + else {- next condition -} if + b + then y + else z + +standaloneComment = + if a + then x + else + {- next condition -} + if b then y else z + +operand = + firstOperand + + if firstCondition + then firstResult + else if secondCondition + then secondResult + else fallback + +=== width 80, indent 2 === +module Main where + +choose = + if firstCondition + then firstResult + else if secondCondition + then secondResult + else if thirdCondition + then thirdResult + else fallback + +short = if a then x else if b then y else z + +blocks = + if firstCondition + then do + firstAction + secondAction + else if secondCondition + then do + thirdAction + else fallback + +commented = + if firstCondition + then firstResult + else -- next condition + if secondCondition then secondResult else fallback + +parenthesized = + if firstCondition + then firstResult + else (if secondCondition then secondResult else fallback) + +nested = + if firstCondition + then if innerCondition then innerResult else innerFallback + else if secondCondition + then secondResult + else fallback + +wrapped = + if a then x else if predicate firstArgument secondArgument then y else z + +inlineComment = if a then x else {- next condition -} if b then y else z + +standaloneComment = + if a + then x + else + {- next condition -} + if b then y else z + +operand = + firstOperand + + if firstCondition + then firstResult + else if secondCondition + then secondResult + else fallback + +=== width 160, indent 2 === +module Main where + +choose = if firstCondition then firstResult else if secondCondition then secondResult else if thirdCondition then thirdResult else fallback + +short = if a then x else if b then y else z + +blocks = + if firstCondition + then do + firstAction + secondAction + else if secondCondition + then do + thirdAction + else fallback + +commented = + if firstCondition + then firstResult + else -- next condition + if secondCondition then secondResult else fallback + +parenthesized = if firstCondition then firstResult else (if secondCondition then secondResult else fallback) + +nested = if firstCondition then if innerCondition then innerResult else innerFallback else if secondCondition then secondResult else fallback + +wrapped = if a then x else if predicate firstArgument secondArgument then y else z + +inlineComment = if a then x else {- next condition -} if b then y else z + +standaloneComment = + if a + then x + else + {- next condition -} + if b then y else z + +operand = firstOperand + if firstCondition then firstResult else if secondCondition then secondResult else fallback + +=== width 40, indent 4, unicode === +module Main where + +choose = + if firstCondition + then firstResult + else if secondCondition + then secondResult + else if thirdCondition + then thirdResult + else fallback + +short = + if a then x else if b then y else z + +blocks = + if firstCondition + then do + firstAction + secondAction + else if secondCondition + then do + thirdAction + else fallback + +commented = + if firstCondition + then firstResult + else -- next condition + if secondCondition + then secondResult + else fallback + +parenthesized = + if firstCondition + then firstResult + else + ( if secondCondition + then secondResult + else fallback + ) + +nested = + if firstCondition + then + if innerCondition + then innerResult + else innerFallback + else if secondCondition + then secondResult + else fallback + +wrapped = + if a + then x + else if + predicate + firstArgument + secondArgument + then y + else z + +inlineComment = + if a + then x + else {- next condition -} if b + then y + else z + +standaloneComment = + if a + then x + else + {- next condition -} + if b then y else z + +operand = + firstOperand + + if firstCondition + then firstResult + else if secondCondition + then secondResult + else fallback From aad73dfea48c18e2d26d9eeaa277be93f2ce6814 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 8 Oct 2026 00:47:27 +0000 Subject: [PATCH 6/6] Destructure validated formatter syntax with slice patterns Replace positional syntax accesses for case, let/ado, where, and forall with named structural patterns. Bind nonempty chain tails and leading else declarations directly instead of repeating indexing assumptions. Preserve formatting behavior, including empty ado bodies and optional where bindings. Keep position-driven delimiter and operator iteration unchanged. Existing formatting expectations require no updates. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-01a11570-3ec2-7539-bc19-3d4825503ee1 --- compiler-frontend/formatting/src/printer.rs | 100 +++++++++++--------- 1 file changed, 57 insertions(+), 43 deletions(-) diff --git a/compiler-frontend/formatting/src/printer.rs b/compiler-frontend/formatting/src/printer.rs index f3f7cf415..15515f8c2 100644 --- a/compiler-frontend/formatting/src/printer.rs +++ b/compiler-frontend/formatting/src/printer.rs @@ -463,16 +463,16 @@ impl<'arena> Printer<'arena, '_> { return Ok(self.arena.nil()); } let (head, tail) = elements.split_at(head_length.min(elements.len())); - if tail.is_empty() { + let [first, ..] = tail else { return self.fixed(head, context); - } + }; let head_context = Context { following: Some(margin), ..context }; let head_document = if let [keyword, constraints] = head && keyword.kind == CLASS && constraints.kind == ClassConstraints && constraints.children.first().is_some_and(|tree| tree.kind == LEFT_PARENTHESIS) { - self.delimited(constraints, head_context, Some((keyword, tail[0])))? + self.delimited(constraints, head_context, Some((keyword, first)))? } else { self.fixed(head, head_context)? }; @@ -523,7 +523,7 @@ impl<'arena> Printer<'arena, '_> { let body = if matches!(gap(0), Gap::Soft) { body } else { body.group() }; Ok(head_document .append( - self.boundary(tail[0].start, gap(0))? + self.boundary(first.start, gap(0))? .append(body) .nest((margin - context.margin) as isize), ) @@ -823,7 +823,10 @@ impl<'arena> Printer<'arena, '_> { ) -> Result>, FormatError> { let expression = tree.expression().unwrap_or(tree); if expression.kind == ExpressionCaseOf { - self.fixed(&expression.elements()[..3], context).map(Some) + let [case_keyword, trunk, of_keyword, _] = expression.elements()[..] else { + unreachable!("invariant violated: validated case expression has four elements"); + }; + self.fixed(&[case_keyword, trunk, of_keyword], context).map(Some) } else { Ok(None) } @@ -834,9 +837,12 @@ impl<'arena> Printer<'arena, '_> { elements: &[&Tree], context: Context, ) -> Result, FormatError> { - let header = self.fixed(&elements[..3], context)?; + let [case_keyword, scrutinees, of_keyword, branches] = elements[..] else { + unreachable!("invariant violated: validated case expression has four elements"); + }; + let header = self.fixed(&[case_keyword, scrutinees, of_keyword], context)?; let margin = context.margin + self.config.indent_width; - let items = elements[1].elements(); + let items = scrutinees.elements(); let mut trunk = self.arena.nil(); for (position, item) in items.iter().enumerate() { let gap = if item.kind == COMMA { Gap::SoftEmpty } else { self.fixed_gap(item.start) }; @@ -848,14 +854,14 @@ impl<'arena> Printer<'arena, '_> { .append(boundary.append(self.tree(item, item_context)?).nest(indentation as isize)); } let broken = self - .tree(elements[0], context)? + .tree(case_keyword, context)? .append( - self.boundary(elements[1].start, Gap::Hard(1))? + self.boundary(scrutinees.start, Gap::Hard(1))? .append(trunk.group()) .nest(self.config.indent_width as isize), ) - .append(self.boundary(elements[2].start, Gap::Hard(1))?) - .append(self.tree(elements[2], context)?); + .append(self.boundary(of_keyword.start, Gap::Hard(1))?) + .append(self.tree(of_keyword, context)?); let arena = self.arena; let probe = arena.fail().flat_alt(header).group(); let width = self.config.line_width; @@ -869,7 +875,7 @@ impl<'arena> Printer<'arena, '_> { broken.clone().into_doc() } }); - Ok(header.append(self.tree(elements[3], context)?)) + Ok(header.append(self.tree(branches, context)?)) } fn where_expression( @@ -878,26 +884,32 @@ impl<'arena> Printer<'arena, '_> { elements: &[&Tree], context: Context, ) -> Result, FormatError> { - let Some(position) = elements.iter().position(|tree| tree.kind == WHERE) else { - return if head.is_empty() { - self.fixed(elements, context) - } else { - self.continuation(head, elements, context) - }; + let (expression, where_keyword, bindings) = match elements { + [expression] => { + return if head.is_empty() { + self.fixed(&[*expression], context) + } else { + self.continuation(head, &[*expression], context) + }; + } + [expression, where_keyword, bindings] => (*expression, *where_keyword, *bindings), + _ => unreachable!( + "invariant violated: validated where expression has one or three elements" + ), }; let margin = context.margin.max(context.following.unwrap_or(0) + self.config.indent_width); let expression_context = Context { following: Some(margin - self.config.indent_width), ..context }; let expression = if head.is_empty() { - self.fixed(&elements[..position], expression_context)? + self.fixed(&[expression], expression_context)? } else { - self.continuation(head, &elements[..position], expression_context)? + self.continuation(head, &[expression], expression_context)? }; let binding_context = Context { margin, following: Some(margin) }; - let bindings = self.block(&elements[position + 1].elements(), binding_context, margin)?; + let bindings = self.block(&bindings.elements(), binding_context, margin)?; Ok(expression.append( - self.boundary(elements[position].start, Gap::Hard(1))? - .append(self.tree(elements[position], binding_context)?) + self.boundary(where_keyword.start, Gap::Hard(1))? + .append(self.tree(where_keyword, binding_context)?) .append(bindings) .nest((margin - context.margin) as isize), )) @@ -1006,11 +1018,13 @@ impl<'arena> Printer<'arena, '_> { } } TypeForall => { - let position = elements.iter().position(|tree| tree.kind == PERIOD).unwrap(); - let head = self.fixed(&elements[..=position], context)?.group(); + let [head @ .., body] = elements.as_slice() else { + unreachable!("invariant violated: validated forall has a body"); + }; + let head = self.fixed(head, context)?.group(); Ok(head - .append(self.boundary(elements[position + 1].start, Gap::Soft)?) - .append(self.fixed(&elements[position + 1..], context)?) + .append(self.boundary(body.start, Gap::Soft)?) + .append(self.fixed(&[*body], context)?) .group()) } ExpressionArray @@ -1085,10 +1099,12 @@ impl<'arena> Printer<'arena, '_> { } WhereExpression => self.where_expression(&[], &elements, context), ExpressionLetIn | ExpressionAdo => { - let position = elements.iter().position(|tree| tree.kind == IN).unwrap(); - let has_body = elements[1..position].iter().any(|tree| tree.start < tree.end); + let [keyword, statements, in_keyword, result] = elements[..] else { + unreachable!("invariant violated: validated let/ado has four elements"); + }; + let has_body = statements.start < statements.end; let head = - self.fixed(&elements[..position], Context { following: None, ..context })?; + self.fixed(&[keyword, statements], Context { following: None, ..context })?; let result_margin = if tree.kind == ExpressionAdo { context.margin + self.config.indent_width } else { @@ -1098,22 +1114,18 @@ impl<'arena> Printer<'arena, '_> { let tail = if tree.kind == ExpressionLetIn { let body_context = Context { margin: context.margin + self.config.indent_width, ..context }; - self.tree(elements[position], context)?.append( - self.boundary(elements[position + 1].start, Gap::Hard(1))? - .append(self.fixed(&elements[position + 1..], body_context)?) + self.tree(in_keyword, context)?.append( + self.boundary(result.start, Gap::Hard(1))? + .append(self.fixed(&[result], body_context)?) .nest(self.config.indent_width as isize), ) } else { - self.continuation( - &elements[position..=position], - &elements[position + 1..], - result_context, - )? + self.continuation(&[in_keyword], &[result], result_context)? }; Ok(head .append( self.boundary( - elements[position].start, + in_keyword.start, if has_body { Gap::Hard(1) } else { Gap::Space }, )? .append(tail) @@ -1123,13 +1135,15 @@ impl<'arena> Printer<'arena, '_> { } InstanceChain => self.sequence(&elements, context, |_, _, _| Gap::Hard(1)), InstanceDeclaration | ClassDeclaration | DeriveDeclaration => { - if elements.first().is_some_and(|tree| tree.kind == ELSE) + if let [else_keyword, rest @ ..] = elements.as_slice() + && let [first, ..] = rest + && else_keyword.kind == ELSE && tree.children.iter().any(|tree| tree.kind == LAYOUT_SEPARATOR) { let tail = self - .boundary(elements[1].start, Gap::Hard(1))? - .append(self.fixed(&elements[1..], context)?); - Ok(self.tree(elements[0], context)?.append(tail)) + .boundary(first.start, Gap::Hard(1))? + .append(self.fixed(rest, context)?); + Ok(self.tree(else_keyword, context)?.append(tail)) } else if let Some(head) = elements.iter().position(|tree| matches!(tree.kind, InstanceHead | ClassHead)) && elements[..head]