Status: decided and landed (747b0b3c, branch fix/issue-1461).
All four items plus the structural pass. The decision, the full
per-language classification table, the before/after measurements, the
double-count check and the corpus delta are in
this comment.
The rule now is: a relational operator scores by use, wherever it
is written; a value-bearing operand scores in a boolean slot. Nine
constructs moved or were added accordingly. One of 1,610 integration
snapshots moved.
Two premises of the body below no longer reproduce. Item 3 shipped
in b2b78598 by the wrapper peel rather than the terminal set, so
if (a as Boolean) already scored 1 before this change. Item 2's
"corpus cost is zero" is true of matches! and false of the arm:
serde's one moved snapshot is if cfg!(no_underscore_consts).
One case could not take this route. Perl's bare /re/ is
relational in role, but an unconditional arm would also fire on the
pattern inside $x =~ /re/, whose =~ is already counted — measured,
that scores the bound form 2. Tracked separately as #1467, which needs
a gated arm rather than an unconditional one.
Summary
Four constructs measured as scoring zero ABC conditions in a boolean slot
where an identifier control scores one — but where counting them is a
judgement call rather than a clear omission. Split out of the
uncounted-boolean-operand sweep on fix/batch-2026-09-13 deliberately, so
the decision is made explicitly rather than inherited from a bulk change.
All measured against if (a) / equivalent at the same cyclomatic.
The four
1. Python walrus — if (n := g()): scores 0, control scores 1
Python::NamedExpression (123) is absent from python_bool_terminal_kinds!.
Why it is a judgement call: NamedExpression already scores on the A
(assignment) axis (src/metrics/abc/python.rs). Counting it on C as well
makes it the only construct in the survey that scores twice across two axes.
That is defensible — the axes are independent, and if (n := g()): genuinely
is a decision on g()'s truth — but it is a policy call about ABC's axes,
not a missing arm.
2. Rust matches! — if matches!(x, Some(_)) scores 0, control scores 1
Rust::MacroInvocation (239) is absent from rust_bool_terminal_kinds!.
Why it is a judgement call: the fix would fire for every macro in a
boolean slot — cfg!, dbg!, todo! — not just matches!. And
src/metrics/abc/rust.rs deliberately excludes macros from Branches.
That is the B axis rather than C, so it does not contradict, but it is a
considered omission in the immediate neighbourhood and any fix should say why
C differs from B.
Note the terminal sets do not otherwise discriminate on return type —
CallExpression counts in a boolean slot whatever it returns, and the Kotlin
infix_expression comment added in #1421 says so explicitly. That argues
for counting it; the macro breadth argues against.
Corpus cost is zero: no serde file uses matches! in a boolean slot.
3. Kotlin as — if (a as Boolean) scores 0, control scores 1
Kotlin::AsExpression (220) is absent from kotlin_bool_terminal_kinds!.
Why it is a judgement call: a cast to Boolean is unusual code, and the
set already carries AsQMARK handling elsewhere. Low value, low risk, but
not obviously right.
4. Groovy <=> — spaceship_expression (257)
Why it is a judgement call, and a different one: <=> returns an
Integer, not a boolean. Counting it as a comparison operator is right —
every other language with a spaceship counts it — but putting it in the
terminal-bool set asserts something else: that its value is a boolean
operand. Those are different claims and the second is false.
If it is counted, the route should be a condition token arm, not the
terminal set.
The structural point underneath all four
Terminal-set membership is narrower than a token arm, and that asymmetry
is now inherited by the fixes already shipped.
A kind in <lang>_bool_terminal_kinds! scores only in a boolean slot
(if / while / do / for / ternary / &&-operand). A comparison
token scores everywhere. So on fix/batch-2026-09-13:
val b = a == c // 1 condition (EQEQ token, counted unconditionally)
val b = a is String // 0 conditions (IsExpression, only reaches the slot)
and identically in C#: var b = x == 1; scores 1, var b = x is int; scores
0, because csharp.rs counts EQEQ unconditionally and has no Is token
arm.
Fitzpatrick Rule 5 scores relational operators by use, not by slot. So
the is / in fixes in #1421 and the C# is fix are arguably half-landed:
correct inside a condition, still zero outside one.
Deciding this settles all four cases above and several already shipped, so it
is probably the thing to decide first.
Related
Summary
Four constructs measured as scoring zero ABC conditions in a boolean slot
where an identifier control scores one — but where counting them is a
judgement call rather than a clear omission. Split out of the
uncounted-boolean-operand sweep on
fix/batch-2026-09-13deliberately, sothe decision is made explicitly rather than inherited from a bulk change.
All measured against
if (a)/ equivalent at the samecyclomatic.The four
1. Python walrus —
if (n := g()):scores 0, control scores 1Python::NamedExpression(123) is absent frompython_bool_terminal_kinds!.Why it is a judgement call:
NamedExpressionalready scores on the A(assignment) axis (
src/metrics/abc/python.rs). Counting it on C as wellmakes it the only construct in the survey that scores twice across two axes.
That is defensible — the axes are independent, and
if (n := g()):genuinelyis a decision on
g()'s truth — but it is a policy call about ABC's axes,not a missing arm.
2. Rust
matches!—if matches!(x, Some(_))scores 0, control scores 1Rust::MacroInvocation(239) is absent fromrust_bool_terminal_kinds!.Why it is a judgement call: the fix would fire for every macro in a
boolean slot —
cfg!,dbg!,todo!— not justmatches!. Andsrc/metrics/abc/rust.rsdeliberately excludes macros from Branches.That is the B axis rather than C, so it does not contradict, but it is a
considered omission in the immediate neighbourhood and any fix should say why
C differs from B.
Note the terminal sets do not otherwise discriminate on return type —
CallExpressioncounts in a boolean slot whatever it returns, and the Kotlininfix_expressioncomment added in #1421 says so explicitly. That arguesfor counting it; the macro breadth argues against.
Corpus cost is zero: no serde file uses
matches!in a boolean slot.3. Kotlin
as—if (a as Boolean)scores 0, control scores 1Kotlin::AsExpression(220) is absent fromkotlin_bool_terminal_kinds!.Why it is a judgement call: a cast to
Booleanis unusual code, and theset already carries
AsQMARKhandling elsewhere. Low value, low risk, butnot obviously right.
4. Groovy
<=>—spaceship_expression(257)Why it is a judgement call, and a different one:
<=>returns anInteger, not a boolean. Counting it as a comparison operator is right —every other language with a spaceship counts it — but putting it in the
terminal-bool set asserts something else: that its value is a boolean
operand. Those are different claims and the second is false.
If it is counted, the route should be a condition token arm, not the
terminal set.
The structural point underneath all four
Terminal-set membership is narrower than a token arm, and that asymmetry
is now inherited by the fixes already shipped.
A kind in
<lang>_bool_terminal_kinds!scores only in a boolean slot(
if/while/do/for/ ternary /&&-operand). A comparisontoken scores everywhere. So on
fix/batch-2026-09-13:and identically in C#:
var b = x == 1;scores 1,var b = x is int;scores0, because
csharp.rscountsEQEQunconditionally and has noIstokenarm.
Fitzpatrick Rule 5 scores relational operators by use, not by slot. So
the
is/infixes in #1421 and the C#isfix are arguably half-landed:correct inside a condition, still zero outside one.
Deciding this settles all four cases above and several already shipped, so it
is probably the thing to decide first.
Related
part of repaying that.
whenarm double-counts its comparison operator #1421 — added Kotlinis/in/ infix to the terminal set.