gren-format-lib

Sorting: exposing lists and import statements

gren-format reorders two things automatically: the names inside an exposing ( ... ) list, and a run of import statements. Both are alphabetical, both are independent of each other, and both must keep every comment attached to whatever it was describing — across arbitrarily many reformats. This document is the authoritative spec for how they behave, including the comment cases, which are where the subtleties live.

The code is Formatter.Logical.SortSymbols (sortExposingLists and sortImportGroups); comment attachment happens earlier, in Formatter.Logical.Comments (see commentHandling.md).


Exposing-list sort

The order

A module’s exposing ( ... ) list and every import’s own exposing ( ... ) list sort into three groups — operators, then types, then plain values — and alphabetically (by base name) within each group. This is always the order, independent of the module’s doc comment.

module Demo exposing (zebra, Kiwi, apple, Mango, (|=))

becomes

module Demo exposing ((|=), Kiwi, Mango, apple, zebra)

An operator exposes as (op); a type exposes as Name or Name(..) (the (..) variant-exposing suffix does not change its sort key). The layout — flat on one line, or one-per-row — follows what you wrote; sorting never changes flat-vs-vertical.

elm-format instead reorders a module’s exposing list to follow the @docs directives in its doc comment when they are present, falling back to alphabetical only when they are absent; gren-format deliberately does not couple the two (a divergence — see Comparison with elm-format, point 3).

Comment handling

Each name in the list can carry comments, and they travel with the name when it moves. Which name a comment belongs to is decided by where the comment starts — specifically, by what its starting row already holds — not by what it says:

Multiline block comments

A {- ... -} that spans multiple source rows is classified by the row its {- starts on, exactly like a single-line comment:


Import-statement sort

Runs and boundaries

import statements sort alphabetically by module name, but only within a run — a stretch of imports with no blank line anywhere in it. A blank line is the only boundary: it never moves, and it splits the imports around it into independently sorted groups. Multi-row imports are fine (a wrapped exposing list does not break a run), and neither do comments.

import Zebra
import Mango
-- a section note
import Kiwi
import Apple

import Delta

becomes

import Apple
-- a section note
import Kiwi
import Mango
import Zebra

import Delta

[Zebra, Mango, Kiwi, Apple] is one run — the comment does not split it — sorted as one; Delta is alone in its own run (blank line above it), so there is nothing to sort. The blank line stays exactly where it was, and the comment travels with Kiwi, the import it leads.

Rows of imports separated only by comments read as one block, which is why a comment is not a boundary. elm-format agrees on this much — it sorts every import as a single list regardless of comments — though it then hoists all the comments above the block and drops the blank lines, which gren-format does not.

Which import a comment travels with

Multiline block comments

Classification again follows the {-’s start row:


What enforces this page

SortingCommentZoo is the fixture that carries every comment shape an exposing list or an import run can hold, in one module. It is registered in the test suite (tests/src/Test/Formatter/Format.gren), so a change to any rule on this page shows up as a diff in SortingCommentZoo.formatted.gren — read that diff before deciding a rule change was intended.

gen-random.py generates import runs and exposing lists with comments in these positions and checks, among its other oracles, author-order invariance: the same module re-emitted with its runs and lists in reversed order must format to the same bytes (GENERATOR.md, “Author-order invariance oracle”). That is the one check that can see a comment attached to the wrong name — the comment-multiset oracle discards positions on purpose, and a wrong-but-stable attachment is still an idempotent fixed point.

The oracle encodes the rules on this page as its two pinned positions: the first slot of an import run (the blank line and the section header above it belong to the position, not the import) and index 0 of an exposing list (a comment before the first name is a header comment, not that name’s). If a rule here changes, those pins have to change with it or the sweeps start reporting false finds.

The generated shapes cover both exposing lists (the module header’s and an import’s own) and import runs, with stacked line-leading comments, a block comment glued onto an import line, trailing comment chains, and multiline block comments — including a chain whose links span rows. GENERATOR.md is the inventory.