Skip to content

Port cadencefmt#4492

Open
turbolent wants to merge 66 commits into
masterfrom
bastian/janez-formatter
Open

Port cadencefmt#4492
turbolent wants to merge 66 commits into
masterfrom
bastian/janez-formatter

Conversation

@turbolent
Copy link
Copy Markdown
Member

@turbolent turbolent commented May 19, 2026

Description

Port @janezpodhostnik's Cadence formatter, https://github.com/janezpodhostnik/cadencefmt, as a new formatter package, preserving Git history.

Removed the corpus for now.

#4493 is a follow-up which removes much of the code duplication (AST-to-prettier-document functions)


  • Targeted PR against master branch
  • Linked to Github issue with discussion and accepted design OR link to spec that describes this work
  • Code follows the standards mentioned here
  • Updated relevant documentation
  • Re-reviewed Files changed in the Github PR explorer
  • Added appropriate labels

janezpodhostnik and others added 30 commits April 29, 2026 14:58
Naive formatter using onflow/cadence parser + ast.Doc() + turbolent/prettier.
CLI reads stdin, formats, writes stdout. Snapshot test harness with 5 cases
and idempotence checks. No comment handling or style overrides yet.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Naive formatter using onflow/cadence parser + ast.Doc() + turbolent/prettier.
CLI reads stdin, formats, writes stdout. Snapshot test harness with 5 cases
and idempotence checks. No comment handling or style overrides yet.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Hand-written lexer extracts all four comment kinds (line, block, doc-line,
doc-block) from source bytes. Correctly handles nested block comments,
string literal skipping, and string template interpolation with nested
parentheses. Adjacent comments without blank lines are grouped together.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements the merge-walk algorithm that binds comment groups to AST nodes
as Leading, Trailing, or SameLine. Recursively descends into node children
for nested comments. Group() now checks for code between comments and
correctly separates end-of-line comments from standalone comments below.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Render package wraps AST Doc output with leading, same-line, and trailing
comments from the CommentMap. Formatter pipeline now scans, groups, attaches,
and renders comments end-to-end. Orphaned comment check after rendering.
Added 6 comment-specific snapshot cases plus comment preservation and
idempotence tests for all cases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Render package wraps AST Doc output with leading, same-line, and trailing
comments from the CommentMap. Formatter pipeline now scans, groups, attaches,
and renders comments end-to-end. Orphaned comment check after rendering.
Added 6 comment-specific snapshot cases plus comment preservation and
idempotence tests for all cases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewriter interface and runner with fixed pass order. Import sorter groups
imports by type (identifier -> address -> string) and sorts within groups.
Deduplication of identical imports. Modifier and paren rewrites deferred
as the parser already enforces canonical modifier order.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewriter interface and runner with fixed pass order. Import sorter groups
imports by type (identifier -> address -> string) and sorts within groups.
Deduplication of identical imports. Modifier and paren rewrites deferred
as the parser already enforces canonical modifier order.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Custom renderers for FunctionDeclaration, CompositeDeclaration,
InterfaceDeclaration, VariableDeclaration, FieldDeclaration, and
SpecialFunctionDeclaration. Access modifiers now render on the same line
as the declaration keyword. Members rendered individually with custom
renderers. Import groups rendered without blank lines within groups.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Custom renderers for FunctionDeclaration, CompositeDeclaration,
InterfaceDeclaration, VariableDeclaration, FieldDeclaration, and
SpecialFunctionDeclaration. Access modifiers now render on the same line
as the declaration keyword. Members rendered individually with custom
renderers. Import groups rendered without blank lines within groups.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verify module re-parses formatted output and compares AST structure against
original. Import reordering handled by comparing imports as multisets.
Verification runs by default on every format call; opt out with SkipVerify
option or --no-verify CLI flag. Round-trip test added to snapshot harness.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FuzzFormat (no panics on arbitrary bytes) and FuzzRoundtrip (idempotence
on valid inputs) fuzz targets seeded from snapshot test cases. Both pass
with ~1M executions. Complex contract formatting verified idempotent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Custom function block renderer interleaves comments between statements.
wrapWithAllComments drains descendant comments from nodes rendered via
upstream Doc(). Drain parameter list and type annotation comments in
custom renderers. Handle event doc with descendant comment drain.
All flow-core-contracts now format successfully and idempotently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Return statements with binary expressions (e.g., ?? nil-coalescing)
now indent the continuation line relative to "return". Non-binary
return expressions (function calls with args) are not affected to
avoid over-indentation. Variable declaration values use Group{Indent{
Line{}, valueDoc}} to indent on break.

Added renderStatement dispatch for custom statement rendering and
three new snapshot tests: return-nil-coalescing, return-simple,
variable-nil-coalescing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Return statements with binary expressions (e.g., ?? nil-coalescing)
now indent the continuation line relative to "return". Non-binary
return expressions (function calls with args) are not affected to
avoid over-indentation. Variable declaration values use Group{Indent{
Line{}, valueDoc}} to indent on break.

Added renderStatement dispatch for custom statement rendering and
three new snapshot tests: return-nil-coalescing, return-simple,
variable-nil-coalescing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Custom renderers for ForStatement, WhileStatement, and IfStatement that
render block bodies by iterating statements with renderStatement (which
interleaves comments correctly). Reusable renderBlock/renderBlockBraces
helpers. Fixes comments between statements in loop bodies being dumped
after the loop. Also fix round-trip verifier to use structural comparison
instead of String() which reflected formatting differences.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Custom renderers for ForStatement, WhileStatement, and IfStatement that
render block bodies by iterating statements with renderStatement (which
interleaves comments correctly). Reusable renderBlock/renderBlockBraces
helpers. Fixes comments between statements in loop bodies being dumped
after the loop. Also fix round-trip verifier to use structural comparison
instead of String() which reflected formatting differences.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Variable declarations inside function bodies now use renderVariable
(via renderStatement dispatch) instead of upstream Doc(). Value expressions
only get Group{Indent{Line{}}} for BinaryExpression (e.g., ??); function
call values render directly without extra Indent, avoiding stacked
indentation on arguments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Variable declarations inside function bodies now use renderVariable
(via renderStatement dispatch) instead of upstream Doc(). Value expressions
only get Group{Indent{Line{}}} for BinaryExpression (e.g., ??); function
call values render directly without extra Indent, avoiding stacked
indentation on arguments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Custom renderAssignmentStatement renders target = value without the
upstream's Group{Indent{value}} wrapper that caused stacked indentation
on function call arguments in assignments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Custom renderAssignmentStatement renders target = value without the
upstream's Group{Indent{value}} wrapper that caused stacked indentation
on function call arguments in assignments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rain

Custom renderIfStatement (from Stage 1) naturally fixed long function
call conditions by giving them their own Group context for line-breaking.
Added test to lock in behavior. Fixed orphaned comments in function call
arguments by draining value expression descendants in renderVariable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rain

Custom renderIfStatement (from Stage 1) naturally fixed long function
call conditions by giving them their own Group context for line-breaking.
Added test to lock in behavior. Fixed orphaned comments in function call
arguments by draining value expression descendants in renderVariable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add corpus tests using flow-core-contracts git submodule with
  format, idempotence, round-trip, and comment preservation checks
- Add justfile with build/test/lint/fuzz/corpus recipes
- Add .envrc (use flake) and .gitignore
- Add README with installation, usage, and development docs
- Switch CI to Nix flake for environment setup (single source of truth)
- Upgrade Go to 1.25 in flake.nix
- Remove development-phase docs (PROGRESS.md, spec, agent prompt)
- Update CLAUDE.md to reflect all changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three idempotence bugs found by FuzzRoundtrip:

1. Comments after the last declaration (no blank line) were classified
   as footer instead of trailing, gaining an extra blank line on
   re-format. Fixed by adding trailing-after-last-sibling logic in
   attachLevel() to mirror the existing between-siblings heuristic.

2. Descendant comments drained inside Indent wrappers for binary
   expression values picked up indentation that wasn't stable across
   re-formats. Fixed by draining descendant comments outside the
   Indent in renderVariable and renderReturnStatement.

3. Import dedup removed entries from the sort input but couldn't
   remove declarations from the AST, leaving orphaned duplicates at
   original indices. Removed dedup — stable sort puts duplicates
   adjacent, which is idempotent.

Also: fix all golangci-lint issues (errcheck, staticcheck, unused),
skip unparseable corpus files, and add -run '^$' to fuzz recipe so
non-fuzz test failures don't block fuzzing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ion comment displacement

Three formatting bugs found during corpus inspection:

1. Blank lines inside indented blocks had trailing whitespace from the
   prettier library's indent emission. Added post-processing step in
   formatter.go to strip whitespace-only lines.

2. Entitlement declarations had access modifier on a separate line
   because the upstream Doc() uses HardLine. Added custom renderers
   for EntitlementDeclaration and EntitlementMappingDeclaration.

3. Comments inside function call argument lists were displaced outside
   the closing paren. Added renderExpression dispatcher with custom
   InvocationExpression rendering that places comments between the
   function name and args inside the argument list.

Also adds UPSTREAM_ISSUES.md documenting onflow/cadence AST issues.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ion comment displacement

Three formatting bugs found during corpus inspection:

1. Blank lines inside indented blocks had trailing whitespace from the
   prettier library's indent emission. Added post-processing step in
   formatter.go to strip whitespace-only lines.

2. Entitlement declarations had access modifier on a separate line
   because the upstream Doc() uses HardLine. Added custom renderers
   for EntitlementDeclaration and EntitlementMappingDeclaration.

3. Comments inside function call argument lists were displaced outside
   the closing paren. Added renderExpression dispatcher with custom
   InvocationExpression rendering that places comments between the
   function name and args inside the argument list.

Also adds UPSTREAM_ISSUES.md documenting onflow/cadence AST issues.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The upstream CastingExpression.Doc() places `as!/as?/as` at the same
indent level as the expression when the line breaks, making it look
like a separate statement. Added custom renderCastingExpression that
wraps the operator + type in Indent for proper continuation indentation.

Also adds CastingExpression.Doc() indent issue to UPSTREAM_ISSUES.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verifies that `as!/as?/as` on a continuation line is indented relative
to the expression, and that short casts stay on one line.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Upgrade onflow/cadence to janez/formatter-related-changes branch which
fixes HardLine→Line for access modifiers, move operator spacing, and
makes Argument a walkable Element.

Changes to adapt:
- Remove renderEntitlement (upstream now handles it correctly)
- Keep renderEntitlementMapping (upstream fix incomplete, no Group wrapper)
- Drain conformance comments and move to first member declaration
  (Walk() now yields conformances as children)
- Update invocation drain to use Argument elements
- Add renderIndentedExpression for while conditions so binary expression
  operator continuations (&&, ||) are indented
- Add BinaryExpression.Doc() indent issue to UPSTREAM_ISSUES.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
janezpodhostnik and others added 20 commits April 30, 2026 13:14
Upstream now fixes CastingExpression, BinaryExpression, FunctionDocument
and EntitlementMappingDeclaration Doc() methods.

Removed custom renderers that are no longer needed:
- renderCastingExpression (upstream adds Indent for as!/as?)
- renderIndentedExpression (upstream BinaryExpression adds Indent)

Kept renderEntitlementMapping (upstream Group fix helps access modifier
but body elements still need our Indent).

Updated golden files for ?? indentation change (upstream BinaryExpression
Indent adds one more level).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ents

Dead code removal:
- Remove QuoteStyle type/field from Options
- Remove unused lineWidth/indent params from render.Program()
- Remove redundant renderCommentGroupInline() (6 call sites updated)
- Remove unused rootURI field from LSP server
- Deduplicate findRepoRoot test helper into testutil_test.go

Code quality:
- Replace string(src)==string(out) with bytes.Equal (CLI + LSP)
- Check os.Stdout.Write errors instead of silently discarding
- Add t.Parallel() to all snapshot/property tests
- Document single-hunk limitation in diff.go

Implement FormatVersion option:
- Add CurrentFormatVersion constant ("1") and Validate() method
- Format() rejects unsupported versions on entry
- Reference version in rewrite.go pass-order comment

Implement KeepBlankLines option:
- Add collapseBlankLines() post-processing step
- Default: 1 (at most 1 consecutive blank line)
- Validated >= 0 in Validate()

Implement StripSemicolons option:
- Add trivia.ScanSemicolons() to detect semicolons in source bytes
- Add render.Context to thread semicolon set through renderer
- renderStatement/renderDeclaration append ";" when StripSemicolons=false
- Default: true (strip semicolons, current behavior)

Fix orphaned comments on entitlement access modifiers:
- Add drainDescendantComments catch-all in renderDeclaration
- Prevents crash on NominalType nodes inside access(X) modifiers
- Pre-existing bug found by fuzzer, not caused by this change

CI/config:
- Add golangci-lint step to CI pipeline
- Update flake.nix from go_1_25 to go_1_26
- Update CONTRIBUTING.md Go version to 1.26+

Documentation:
- Update CLAUDE.md, CONTRIBUTING.md, README.md for all changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ents

Dead code removal:
- Remove QuoteStyle type/field from Options
- Remove unused lineWidth/indent params from render.Program()
- Remove redundant renderCommentGroupInline() (6 call sites updated)
- Remove unused rootURI field from LSP server
- Deduplicate findRepoRoot test helper into testutil_test.go

Code quality:
- Replace string(src)==string(out) with bytes.Equal (CLI + LSP)
- Check os.Stdout.Write errors instead of silently discarding
- Add t.Parallel() to all snapshot/property tests
- Document single-hunk limitation in diff.go

Implement FormatVersion option:
- Add CurrentFormatVersion constant ("1") and Validate() method
- Format() rejects unsupported versions on entry
- Reference version in rewrite.go pass-order comment

Implement KeepBlankLines option:
- Add collapseBlankLines() post-processing step
- Default: 1 (at most 1 consecutive blank line)
- Validated >= 0 in Validate()

Implement StripSemicolons option:
- Add trivia.ScanSemicolons() to detect semicolons in source bytes
- Add render.Context to thread semicolon set through renderer
- renderStatement/renderDeclaration append ";" when StripSemicolons=false
- Default: true (strip semicolons, current behavior)

Fix orphaned comments on entitlement access modifiers:
- Add drainDescendantComments catch-all in renderDeclaration
- Prevents crash on NominalType nodes inside access(X) modifiers
- Pre-existing bug found by fuzzer, not caused by this change

CI/config:
- Add golangci-lint step to CI pipeline
- Update flake.nix from go_1_25 to go_1_26
- Update CONTRIBUTING.md Go version to 1.26+

Documentation:
- Update CLAUDE.md, CONTRIBUTING.md, README.md for all changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comments between an entitlement access modifier (e.g., access(A)) and
the declaration keyword were misattached to the NominalType node inside
the access modifier, causing orphaned comments and idempotence failures.

Root cause: getChildren in attach.go yielded NominalType nodes from
the access modifier as siblings. The attachment heuristic then classified
nearby comments as trailing on the NominalType, which no renderer
consumed.

Fix:
- getChildren now excludes elements yielded by DeclarationAccess().Walk()
  so they don't participate in comment classification
- renderAccess helper drains any residual comments on access modifier
  children as a safety net, replacing the raw Access.Doc() call in all
  8 renderers

Found by FuzzRoundtrip.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- declSeparation: "just a newline" → "no blank line" (accurate in prettier terms)
- scanLineComment: clarify position must be at first '/' of '//'
- attach heuristic: make the three disambiguation cases more explicit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add Go benchmarks for end-to-end formatting (snapshots, corpus by size
bucket, largest file) and per-stage breakdown (parse, trivia, render,
pretty-print, verify). Add just recipes: bench, bench-all, bench-stages.

Wire up the --no-verify CLI flag which was registered but never read,
so it actually sets format.Options.SkipVerify. Fix README inaccuracies:
--check prints changed paths (not silent), add --version flag docs, fix
misleading "Go API" phrasing, add -- separator example.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Blank lines separating logical groups of statements inside function and
init bodies were removed during formatting. The renderer now detects
blank lines by scanning source bytes between statements (accounting for
trailing/leading comments via CommentMap offsets) and emits an extra
HardLine to preserve them.

Source bytes are threaded through render.Context so the blank line check
uses actual byte scanning rather than AST line numbers, which can be
inaccurate for multi-line expressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Blank lines separating logical groups of statements inside function and
init bodies were removed during formatting. The renderer now detects
blank lines by scanning source bytes between statements (accounting for
trailing/leading comments via CommentMap offsets) and emits an extra
HardLine to preserve them.

Source bytes are threaded through render.Context so the blank line check
uses actual byte scanning rather than AST line numbers, which can be
inaccurate for multi-line expressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the overlapping `Indent string` and `UseTabs bool` options with
`IndentCharacter string` and `IndentCount int`. This is cleaner — UseTabs
silently ignored Indent, and the new fields map directly to how users
think about indentation. Add validation (character must be " " or "\t",
count >= 1).

Add 11 new tests covering all format options: indent variations (default,
2-space, 3-space, tabs, idempotent), validation (invalid char, zero
count), LineWidth (narrow/wide), SortImports=false, and SkipVerify.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire SortImports through to rewrite.Apply so setting SortImports=false
actually skips import sorting. Previously the option existed but was
dead code.

Add 10 CLI integration tests covering stdin, -w, -c (clean/dirty), -d,
--stdin-filename, --version, --no-verify, parse errors, and directory
recursive formatting. Tests build the binary once and run it via
exec.Command with temp files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
A `//` line comment attached as `SameLine` or `Trailing` of a
TypeAnnotation inside a VariableDeclaration was rendered followed by
` = <value>` on the same doc line, letting the comment swallow the
assignment in output and breaking idempotence (and round-trip in the
worst case — `parse(format-2-output)` lost the value entirely).

Hoist any `//`-style same-line or trailing comment off TypeAnnotation
into Leading-of-value before rendering. Source order is preserved by
moving in reverse-source order with prepend semantics. Also break
before the value (HardLine instead of Space) when the value carries a
leading line comment, so `//` lands on its own line.

Adds two CommentMap helpers for the hoist:
- HasLeadingLineComment (peek without taking)
- MoveSameLineLineCommentToLeading
- MoveTrailingLineCommentsToLeading

Fixes 3 fuzz failures (preserved as regression seeds):
- 0f1d42ffe564c745: `let A:A=\n\n//\n0`
- cc5f86b8ce6f3bb3: `#//\n0//\n#0`
- b55e4b1d068b21cf: `let A:A=//\n//0\n0%0`

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A `//` line comment attached as `SameLine` or `Trailing` of a
TypeAnnotation inside a VariableDeclaration was rendered followed by
` = <value>` on the same doc line, letting the comment swallow the
assignment in output and breaking idempotence (and round-trip in the
worst case — `parse(format-2-output)` lost the value entirely).

Hoist any `//`-style same-line or trailing comment off TypeAnnotation
into Leading-of-value before rendering. Source order is preserved by
moving in reverse-source order with prepend semantics. Also break
before the value (HardLine instead of Space) when the value carries a
leading line comment, so `//` lands on its own line.

Adds two CommentMap helpers for the hoist:
- HasLeadingLineComment (peek without taking)
- MoveSameLineLineCommentToLeading
- MoveTrailingLineCommentsToLeading

Fixes 3 fuzz failures (preserved as regression seeds):
- 0f1d42ffe564c745: `let A:A=\n\n//\n0`
- cc5f86b8ce6f3bb3: `#//\n0//\n#0`
- b55e4b1d068b21cf: `let A:A=//\n//0\n0%0`

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The upstream Cadence parser reports some node end positions past their
syntactic close to the next token's whitespace. Concretely,
parseVoidExpression sets the empty `()` expression's EndPos to
`p.current.EndPos` AFTER consuming `)`, which on multi-line input is the
start of the next line's indent. PragmaDeclaration propagates that end.

Without compensation, attach.go's sameLine and between-sibling checks
register a comment on the line after such a node as SameLine of the
node — which differs from how a re-parse classifies the same comment
once the formatter has normalized the layout, breaking idempotence.

Add `trueEndPosition` helper that walks the source bytes back from the
reported end to the last non-whitespace byte (and tracks line accordingly).
Use it for sameLine and between-sibling decisions; keep the un-clipped
end for the inside check so comments physically inside the un-clipped
span still attach to descendants.

Also generalize the prior MoveTrailingLineCommentsToLeading and
MoveSameLineLineCommentToLeading helpers to all comment kinds (the
idempotence-flip applies to block comments too even though they don't
swallow tokens).

Fixes 2 fuzz failures (preserved as regression seeds):
- f1317c40fc90d7b9: `struct A{access(A)A:A#(//\n)}`
- 59ed9ab21a41e477: `let A:A=\n\n/**/0`

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The upstream Cadence parser reports some node end positions past their
syntactic close to the next token's whitespace. Concretely,
parseVoidExpression sets the empty `()` expression's EndPos to
`p.current.EndPos` AFTER consuming `)`, which on multi-line input is the
start of the next line's indent. PragmaDeclaration propagates that end.

Without compensation, attach.go's sameLine and between-sibling checks
register a comment on the line after such a node as SameLine of the
node — which differs from how a re-parse classifies the same comment
once the formatter has normalized the layout, breaking idempotence.

Add `trueEndPosition` helper that walks the source bytes back from the
reported end to the last non-whitespace byte (and tracks line accordingly).
Use it for sameLine and between-sibling decisions; keep the un-clipped
end for the inside check so comments physically inside the un-clipped
span still attach to descendants.

Also generalize the prior MoveTrailingLineCommentsToLeading and
MoveSameLineLineCommentToLeading helpers to all comment kinds (the
idempotence-flip applies to block comments too even though they don't
swallow tokens).

Fixes 2 fuzz failures (preserved as regression seeds):
- f1317c40fc90d7b9: `struct A{access(A)A:A#(//\n)}`
- 59ed9ab21a41e477: `let A:A=\n\n/**/0`

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six independent cleanups, all verified against snapshot, corpus,
idempotence, round-trip, and benchmark suites (no perf regression):

1. rewrite: export ImportGroupOrder; render now reuses it instead of
   maintaining a duplicate importGroupType.

2. format: introduce ErrParse and ErrInternal sentinel errors.
   formatter.go wraps with %w; cmd/cadencefmt uses errors.Is to choose
   exit codes 3 vs 4. Replaces the brittle strings.Contains check on
   error messages. Side benefit: rewrite failures now correctly produce
   exit code 4 (was silently demoted to 3).

3. render: extract renderConformances helper, collapsing a 15-line
   block duplicated between renderComposite and renderInterface.

4. cli: reject mutually-exclusive flag combinations (-w -c, -c -d,
   -w -d, stdin + -w) with exit code 2 and a clear message. Previously
   silent precedence (check > diff > write) was a boolean trap.

5. render: replace single-method walkable interface in trivia.go with
   a func(func(ast.Element)) parameter; call sites pass .Walk as a
   bound method value.

6. render: collapse Context+cm threading into a private renderer struct
   holding cm/source/semicolons. All 22+4 declaration/expression
   functions become methods on *renderer. Public Program signature is
   now Program(prog, cm, source, semicolons). Deletes context.go;
   adds renderer.go.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
git-subtree-dir: formatter
git-subtree-mainline: b471d3f
git-subtree-split: 5969f8d
…8f5906e9e5d2d2ea9'

git-subtree-dir: formatter/testdata/format
git-subtree-mainline: 1d148b1
git-subtree-split: 0f8855b
@turbolent turbolent requested a review from SupunS as a code owner May 19, 2026 23:14
@turbolent turbolent self-assigned this May 19, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 19, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 19, 2026

Benchstat comparison

  • Base branch: onflow:master
  • Base commit: a9c132b
Results

old.txtnew.txt
time/opdelta
pkg:github.com/onflow/cadence/interpreter goos:linux goarch:amd64
ByteArrayTransfer-41.26µs ± 0%1.31µs ± 0%~(p=1.000 n=1+1)
ByteArrayValueToByteSlice-483.6ns ± 0%83.1ns ± 0%~(p=1.000 n=1+1)
ByteSliceToByteArrayValue-4928ns ± 0%1036ns ± 0%~(p=1.000 n=1+1)
pkg:github.com/onflow/cadence/runtime goos:linux goarch:amd64
ContractFunctionInvocation-4394µs ± 0%395µs ± 0%~(p=1.000 n=1+1)
pkg:github.com/onflow/cadence/interpreter goos:linux goarch:amd64
EMVAddressTransfer-43.45µs ± 0%3.35µs ± 0%~(p=1.000 n=1+1)
Emit-44.82ms ± 0%4.83ms ± 0%~(p=1.000 n=1+1)
EnumTransfer-41.40µs ± 0%1.31µs ± 0%~(p=1.000 n=1+1)
pkg:github.com/onflow/cadence/runtime goos:linux goarch:amd64
ExportType/composite_type-4262ns ± 0%256ns ± 0%~(p=1.000 n=1+1)
ExportType/simple_type-477.8ns ± 0%77.6ns ± 0%~(p=1.000 n=1+1)
pkg:github.com/onflow/cadence/interpreter goos:linux goarch:amd64
ImperativeFib-425.3µs ± 0%25.2µs ± 0%~(p=1.000 n=1+1)
InterpretRecursionFib-42.40ms ± 0%2.48ms ± 0%~(p=1.000 n=1+1)
NewInterpreter/new_interpreter-4957ns ± 0%981ns ± 0%~(p=1.000 n=1+1)
NewInterpreter/new_sub-interpreter-4350ns ± 0%347ns ± 0%~(p=1.000 n=1+1)
pkg:github.com/onflow/cadence/runtime goos:linux goarch:amd64
RuntimeFungibleTokenTransferInterpreter-4639µs ± 0%635µs ± 0%~(p=1.000 n=1+1)
RuntimeResourceDictionaryValues-42.69ms ± 0%2.69ms ± 0%~(p=1.000 n=1+1)
RuntimeResourceTracking-49.74ms ± 0%9.76ms ± 0%~(p=1.000 n=1+1)
RuntimeScriptNoop-414.7µs ± 0%14.5µs ± 0%~(p=1.000 n=1+1)
RuntimeVMInvokeContractImperativeFib-446.1µs ± 0%44.5µs ± 0%~(p=1.000 n=1+1)
pkg:github.com/onflow/cadence/interpreter goos:linux goarch:amd64
ValueIsSubtypeOfSemaType-464.1ns ± 0%67.5ns ± 0%~(p=1.000 n=1+1)
 
alloc/opdelta
pkg:github.com/onflow/cadence/interpreter goos:linux goarch:amd64
ByteArrayTransfer-41.07kB ± 0%1.08kB ± 0%~(p=1.000 n=1+1)
ByteArrayValueToByteSlice-432.0B ± 0%32.0B ± 0%~(all equal)
ByteSliceToByteArrayValue-4858B ± 0%868B ± 0%~(p=1.000 n=1+1)
pkg:github.com/onflow/cadence/runtime goos:linux goarch:amd64
ContractFunctionInvocation-4145kB ± 0%146kB ± 0%~(p=1.000 n=1+1)
pkg:github.com/onflow/cadence/interpreter goos:linux goarch:amd64
EMVAddressTransfer-42.46kB ± 0%2.46kB ± 0%~(p=1.000 n=1+1)
Emit-41.53MB ± 0%1.53MB ± 0%~(p=1.000 n=1+1)
EnumTransfer-4837B ± 0%850B ± 0%~(p=1.000 n=1+1)
pkg:github.com/onflow/cadence/runtime goos:linux goarch:amd64
ExportType/composite_type-4120B ± 0%120B ± 0%~(all equal)
ExportType/simple_type-40.00B 0.00B ~(all equal)
pkg:github.com/onflow/cadence/interpreter goos:linux goarch:amd64
ImperativeFib-48.30kB ± 0%8.30kB ± 0%~(all equal)
InterpretRecursionFib-41.19MB ± 0%1.19MB ± 0%~(p=1.000 n=1+1)
NewInterpreter/new_interpreter-4976B ± 0%976B ± 0%~(all equal)
NewInterpreter/new_sub-interpreter-4232B ± 0%232B ± 0%~(all equal)
pkg:github.com/onflow/cadence/runtime goos:linux goarch:amd64
RuntimeFungibleTokenTransferInterpreter-4174kB ± 0%174kB ± 0%~(p=1.000 n=1+1)
RuntimeResourceDictionaryValues-41.77MB ± 0%1.77MB ± 0%~(p=1.000 n=1+1)
RuntimeResourceTracking-46.98MB ± 0%6.99MB ± 0%~(p=1.000 n=1+1)
RuntimeScriptNoop-48.09kB ± 0%8.09kB ± 0%~(p=1.000 n=1+1)
RuntimeVMInvokeContractImperativeFib-414.3kB ± 0%14.3kB ± 0%~(p=1.000 n=1+1)
pkg:github.com/onflow/cadence/interpreter goos:linux goarch:amd64
ValueIsSubtypeOfSemaType-432.0B ± 0%32.0B ± 0%~(all equal)
 
allocs/opdelta
pkg:github.com/onflow/cadence/interpreter goos:linux goarch:amd64
ByteArrayTransfer-47.00 ± 0%7.00 ± 0%~(all equal)
ByteArrayValueToByteSlice-41.00 ± 0%1.00 ± 0%~(all equal)
ByteSliceToByteArrayValue-45.00 ± 0%5.00 ± 0%~(all equal)
pkg:github.com/onflow/cadence/runtime goos:linux goarch:amd64
ContractFunctionInvocation-42.27k ± 0%2.27k ± 0%~(all equal)
pkg:github.com/onflow/cadence/interpreter goos:linux goarch:amd64
EMVAddressTransfer-429.0 ± 0%29.0 ± 0%~(all equal)
Emit-441.0k ± 0%41.0k ± 0%~(all equal)
EnumTransfer-413.0 ± 0%13.0 ± 0%~(all equal)
pkg:github.com/onflow/cadence/runtime goos:linux goarch:amd64
ExportType/composite_type-43.00 ± 0%3.00 ± 0%~(all equal)
ExportType/simple_type-40.00 0.00 ~(all equal)
pkg:github.com/onflow/cadence/interpreter goos:linux goarch:amd64
ImperativeFib-4176 ± 0%176 ± 0%~(all equal)
InterpretRecursionFib-417.7k ± 0%17.7k ± 0%~(all equal)
NewInterpreter/new_interpreter-415.0 ± 0%15.0 ± 0%~(all equal)
NewInterpreter/new_sub-interpreter-44.00 ± 0%4.00 ± 0%~(all equal)
pkg:github.com/onflow/cadence/runtime goos:linux goarch:amd64
RuntimeFungibleTokenTransferInterpreter-43.24k ± 0%3.24k ± 0%~(all equal)
RuntimeResourceDictionaryValues-436.7k ± 0%36.7k ± 0%~(all equal)
RuntimeResourceTracking-4129k ± 0%129k ± 0%~(p=1.000 n=1+1)
RuntimeScriptNoop-4114 ± 0%114 ± 0%~(all equal)
RuntimeVMInvokeContractImperativeFib-4481 ± 0%481 ± 0%~(all equal)
pkg:github.com/onflow/cadence/interpreter goos:linux goarch:amd64
ValueIsSubtypeOfSemaType-41.00 ± 0%1.00 ± 0%~(all equal)
 

@turbolent turbolent requested a review from janezpodhostnik May 20, 2026 16:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants