Skip to content

luafmt is not idempotent: layout of a nested call alternates between two states forever #1302

Description

@mareklangiewicz

This input never stabilizes: from the 3rd pass on, luafmt flips between two layouts forever (pass 3 == pass 5, pass 3 != pass 4), so luafmt --check fails on its own output.

Save as osc.lua:

function M.evidence(rows)
  local facts = Seq.from(rows)
    :map(function(row)
      local p = row[2]
      return Seq.concat(
        { p.tail ~= '' and p.tail or '[no name]', 'ft=' .. (p.filetype ~= '' and p.filetype or '-') },
        {
          'lines=' .. p.lines,
        },
        p.marks ~= nil and { 'marks=' .. p.marks } or {},
        p.modified and { 'MODIFIED' } or {}
      ):join '  '
    end)
    :totable()
end
cp osc.lua o0.lua
for i in 1 2 3 4 5; do luafmt --max-line-width 120 < o$((i-1)).lua > o$i.lua; done
cmp o3.lua o5.lua && ! cmp -s o3.lua o4.lua && echo '2-cycle'

The two states alternate between the argument list being "packed" on the Seq.concat( line and fully expanded one-per-line:

      return Seq.concat({ p.tail ~= '' and p.tail or '[no name]', 'ft=' .. (p.filetype ~= '' and p.filetype or '-') }, {
        'lines=' .. p.lines,
      }, p.marks ~= nil and { 'marks=' .. p.marks } or {}, p.modified and { 'MODIFIED' } or {}
      ):join '  '

Without the enclosing function ... end (two columns less indentation) it converges after one pass. The first pass also emitted a 123-column line elsewhere in the same codebase with max_line_width = 120, which the second pass then split.

On a 165-file codebase, 25 files changed again on the second pass, 5 on the third, and 2 kept alternating. --verify reports this, but there's no way to reach a fixed point.

luafmt 0.25.1.

Activity

  1. added theissue type on Oct 16, 2026
  2. changed the issue type fromtoon Oct 16, 2026
  3. mareklangiewicz commented on Oct 16, 2026

    @mareklangiewicz
    ContributorAuthor

    Correction to the repro above: --max-line-width 120 alone converges. It needs 2-space indent as well, and only widths 118–120 cycle:

    for i in 1 2 3 4 5; do luafmt --spaces 2 --max-line-width 120 < o$((i-1)).lua > o$i.lua; done

    Cause and fix in #1305: the call-arg layout estimated the first line from the first argument's source column, which the two layouts move.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions