You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Comment preservation is guarded rule-by-rule, not systemically — fmt deletes and relocates comments in declaration headers, and 8 of 9 fix-emitting lint rules have no guard #97
Comment preservation is guarded rule-by-rule and position-by-position, never systemically — so new positions keep losing comments. Three fresh instances below, plus the structural reason they were not caught.
The contract being broken
docs/tmt/fmt.md: "Both rewrites are whitespace-only, which is what makes --check a safe…". docs/pmt/fmt.md says the same of its grid. A rewrite that moves a comment across significant tokens is not whitespace-only; one that deletes it is further still.
For lint, #81 established the posture explicitly: "finding always reported; fix withheld when the deletion span contains a comment token", with the infrastructure to do it (comment_tokens on the lint context, span_touches_a_comment).
Three measured defects
1. pmt fmt DELETES a comment between a function's name and its (.
grep KEEPME on the output returns nothing. Same for main // KEEPME on its own line. This is silent data loss under format-on-save.
2. tmt fmt RELOCATES a comment between a declaration's keyword and its name, and the result is not idempotent.
alphabet /* c */ ab { '_' }
→ alphabet ab { /* c */ '_' } (pass 1 — crosses `ab` and `{`)
→ alphabet ab { /* c */
'_'
} (pass 2 — moves again, reflows)
Same shape elsewhere: routine /* c */ r(tape t: ab) puts the comment inside the signature parens; tape /* c */ main: ab; puts it after the whole statement. Only a comment written before the keyword stays put. Note this is distinct from the documented paren_list/with-map rendering gap — that one breaks a list to multi-line without moving anything across tokens.
3. The leftover-debugger quickfix deletes an interior comment.
Applied text of
['1'] -> debugger /* keep me */ move [>] goto s;
is
['1'] -> move [>] goto s;
The comment is not part of the marker being removed. This is exactly the class #81 fixed for duplicate-map-source and contract-clause-overlap.
Why this keeps happening
The guard is per-rule and per-position, so each fix covers one site:
Measured now: nine .tmc lint rules emit a Fix, and exactly one — contract_clause_overlap — carries the comment guard. The other eight compute a byte span from the comment-free token stream and delete it from the original source, so any comment inside that span goes with it. leftover-debugger is the one measured above; unused-graft-name is the clearest next candidate, since its span is documented as running "from the end of the binding's closing ) through the end of the instance name" and a comment written there sits inside it.
Nothing tests comment preservation anywhere. There is no property asserting that every comment in the input appears in the output, for either language. And crates/post-machine/tests/fmt_property.rs's generator is deliberately LINE-ORIENTED by its own module doc — comments only on their own line or trailing — so it can never generate a comment inside a declaration header, which is where defects 1 and 2 live. A guard whose generator cannot produce the failing case is not a guard.
Suggested shape
One guard rather than N point fixes, or the next new position repeats this:
Generalize the withhold posture from contract_clause_overlap to every fix-emitting rule, rather than adding it to one rule at a time. A shared helper plus a test that enumerates fix-emitting rules and asserts each is guarded — the same set-compare-in-both-directions discipline the completions registry and the error-code registries already use.
Defect 2's fix has a natural home in the .tmc formatter's port onto the green tree, which is upcoming work; defects 1 and 3 are independent of it.
Not verified
Whether defect 1 predates pmt fmt's move onto the green tree. Settled by building pmt from master and re-running the two-line reproducer.
Comment preservation is guarded rule-by-rule and position-by-position, never systemically — so new positions keep losing comments. Three fresh instances below, plus the structural reason they were not caught.
The contract being broken
docs/tmt/fmt.md: "Both rewrites are whitespace-only, which is what makes--checka safe…".docs/pmt/fmt.mdsays the same of its grid. A rewrite that moves a comment across significant tokens is not whitespace-only; one that deletes it is further still.For lint, #81 established the posture explicitly: "finding always reported; fix withheld when the deletion span contains a comment token", with the infrastructure to do it (
comment_tokenson the lint context,span_touches_a_comment).Three measured defects
1.
pmt fmtDELETES a comment between a function's name and its(.grep KEEPMEon the output returns nothing. Same formain // KEEPMEon its own line. This is silent data loss under format-on-save.2.
tmt fmtRELOCATES a comment between a declaration's keyword and its name, and the result is not idempotent.Same shape elsewhere:
routine /* c */ r(tape t: ab)puts the comment inside the signature parens;tape /* c */ main: ab;puts it after the whole statement. Only a comment written before the keyword stays put. Note this is distinct from the documentedparen_list/with-maprendering gap — that one breaks a list to multi-line without moving anything across tokens.3. The
leftover-debuggerquickfix deletes an interior comment.Applied text of
is
The comment is not part of the marker being removed. This is exactly the class #81 fixed for
duplicate-map-sourceandcontract-clause-overlap.Why this keeps happening
The guard is per-rule and per-position, so each fix covers one site:
.tmacomments relocateduplicate-map-source's quickfix deletes interior commentsMeasured now: nine
.tmclint rules emit aFix, and exactly one —contract_clause_overlap— carries the comment guard. The other eight compute a byte span from the comment-free token stream and delete it from the original source, so any comment inside that span goes with it.leftover-debuggeris the one measured above;unused-graft-nameis the clearest next candidate, since its span is documented as running "from the end of the binding's closing)through the end of the instance name" and a comment written there sits inside it.Nothing tests comment preservation anywhere. There is no property asserting that every comment in the input appears in the output, for either language. And
crates/post-machine/tests/fmt_property.rs's generator is deliberately LINE-ORIENTED by its own module doc — comments only on their own line or trailing — so it can never generate a comment inside a declaration header, which is where defects 1 and 2 live. A guard whose generator cannot produce the failing case is not a guard.Suggested shape
One guard rather than N point fixes, or the next new position repeats this:
contract_clause_overlapto every fix-emitting rule, rather than adding it to one rule at a time. A shared helper plus a test that enumerates fix-emitting rules and asserts each is guarded — the same set-compare-in-both-directions discipline the completions registry and the error-code registries already use.Defect 2's fix has a natural home in the
.tmcformatter's port onto the green tree, which is upcoming work; defects 1 and 3 are independent of it.Not verified
Whether defect 1 predates
pmt fmt's move onto the green tree. Settled by buildingpmtfrommasterand re-running the two-line reproducer.