Skip to content

decide: four uncounted boolean operands, and whether terminal sets should be token arms #1461

Description

@dekobon

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 asif (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

Activity

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions