Two negations, one meaning
Inverting a boolean predicate requires changing both its body and its uses. Three direct-call shapes explain the negations and parentheses; a September prod-code implementation also shows the limits of reference discovery and textual rewriting.
On this page · 4 sections
Some predicates are named the wrong way round. A codebase full of !is_disabled(x) reads better as
is_enabled(x), and a check that is nearly always negated at its call sites wants to be the other
check. The refactoring is on this project’s catalog as invert_boolean: give the function the
opposite name and meaning, with the aim of preserving the meaning of its uses. Doing that
for a few direct calls is simpler than establishing it for an entire program.
This post follows the Rust implementation merged on September 23, 2026 in PR #116, pinned at 7129be17. The command output, timings and development-test outcomes below are historical PR reports, not executions independently repeated for this post. Later versions changed the tool.
The probe recorded in PR #108 did not offer
this refactoring at the tested positions. It offered convert_bool_to_enum at a boolean
binding - a different change. This is a result for those probes, not every analyzer version
or possible trigger. code_invert_boolean implemented the operation in two halves.
The body
The function must now return the opposite of what it returned. A body that is one expression is negated in place:
-pub fn is_small(n: u32) -> bool {
- n < 10
+pub fn is_large(n: u32) -> bool {
+ !(n < 10)
A body with statements is negated as a block - !{ … } negates the block’s value, which is its
tail - and every return in it is negated too, because a return leaves before the tail is
reached. Every return of the function, that is: a return inside a closure or an async block
returns from that closure or block, and one inside a nested fn returns from that function, so
the implementation tries to leave those alone. Its selection of return sites scans text,
using braces and nearby |, async and fn spellings. The unit test writes out specific
closure, async-block and nested-function shapes; that does not establish correct handling
of every valid Rust form.
The calls
The implementation processes references returned by the analyzer. For a reference it can read as a call, the proposed edit depends on what is around it:
A plain call gains a !. A call that already had a ! loses it instead: !!is_large(n) would be
correct and nobody would write it. And a call that goes on - is_small(n).then(|| n) - cannot just
gain a ! in front, because method calls bind more tightly than unary !: !is_large(n).then(|| n) negates
whatever .then returns, which here is an Option and does not compile, and in other cases would
compile and mean something else. The proposed edit is (!is_large(n)).then(|| n). This
revision uses the next non-whitespace character (., ? or [) to select parentheses;
it is a textual rule rather than a complete expression-tree transformation.
PR #116 reports this scratch-crate result. The excerpt is abridged: it omits the changed-line count, file headers and final apply instruction. The body diff is included here:
`is_small` → `is_large` (src/lib.rs)
- the body returns the negation of what it returned
- 2 call(s) gain a `!`, 1 lose the `!` they had
- n < 10
+ !(n < 10)
- if is_small(n) {
+ if !is_large(n) {
- } else if !is_small(n + 1) {
+ } else if is_large(n + 1) {
- is_small(n).then(|| n)
+ (!is_large(n)).then(|| n)
the analyzer accepts the result: 0 errors
The PR reports a warm dry-run duration of 0.68 seconds for this example. It is a single reported observation, not a general latency guarantee or evidence of an apply run.
The reference that is not a call
One reference shape this implementation leaves unchanged is the function used as a value, filter(is_small) or a
fn(u32) -> bool returned from somewhere. Under the new name it would mean the opposite, and there
is no call to put a ! in front of - one possible adapter is a closure, |n| !is_large(n), and whether the
place that holds the function wants the old meaning or the new one is not something the text says.
This revision lists the reference as unmatched and leaves it untouched, including its old
name. If that name no longer resolves after the declaration is renamed, the remaining use
needs repair; merely listing it does not make the result valid.
The code validates the rewritten files in an analyzer overlay. An apply request is refused
when that check returns error diagnostics, unless force is set. There is no separate
unmatched gate. A file containing only an unmatched use need not be in the rewritten set,
so this path does not guarantee that the unresolved use will be diagnosed. A dry run writes
nothing regardless of those results. verify: "compile" was an additional option; an empty
analyzer report alone is not compilation or behaviour-preservation proof.
When a returned reference points inside the predicate’s own body, this implementation refuses the operation. The test includes such a recursive case. This still depends on receiving the reference: the historical code turns a failed reference query into an empty list. Neither that empty list nor the unmatched list proves that all uses have been accounted for.
Tests and later scope
The EVEN fixture in the PR’s orchestration test contains an early return, direct and
negated calls, a continued .then_some(n) call and a function-value reference. It uses a
scripted gateway that supplies references and no analyzer diagnostics. The test asserts the
rewritten strings, an unmatched count of one, and a successful apply. That checks orchestration
under those answers; it does not demonstrate that a real analyzer accepts the leftover old name.
A second fixture checks refusal when a recursive reference is supplied.
PR #116 reports that the first run failed because a supplied reference column pointed at
s_even rather than is_even. It also reports that disabling negation cancellation made the
counts (3, 0) instead of (2, 1). These development and mutation-check outcomes are attributed
to the PR; the available feature/test commit and source assertions are not independent run receipts.
We have not rerun those checks.
The following boundaries belong to that historical implementation:
- Fields and variables came later. This revision targeted functions. PR #135 added boolean fields and locals at 06:38 UTC on September 23, 2026, later the same morning.
- Names. The new name is given, not derived;
is_validdoes not becomeis_invalidby itself. - Rust-only is historical. PR #707 added other languages on September 29; 080971c9 later corrected declaration matching in that path. This post does not assess the correctness of those later implementations.
Cite this article
Alexander Panasenko (2026-10-17). Two negations, one meaning. https://prod.codes/blog/two-negations-one-meaning/