Skip to content

Fix C++ ternary operator type checking for bitfield operands#8792

Open
tautschnig wants to merge 2 commits into
diffblue:developfrom
tautschnig:fix-933-ternary-cpp
Open

Fix C++ ternary operator type checking for bitfield operands#8792
tautschnig wants to merge 2 commits into
diffblue:developfrom
tautschnig:fix-933-ternary-cpp

Conversation

@tautschnig
Copy link
Copy Markdown
Collaborator

implicit_conversion_sequence does not guarantee to produce the exact type requested as bitfields promote to the underlying type. Therefore, add typecasts as needed to ensure type consistency.

Fixes: #933

  • Each commit message has a non-empty body, explaining why the change was made.
  • n/a Methods or procedures I have added are documented, following the guidelines provided in CODING_STANDARD.md.
  • n/a The feature or user visible behaviour I have added or modified has been documented in the User Guide in doc/cprover-manual/
  • Regression or unit tests are included, or existing tests cover the modified code (in this case I have detailed which ones those are in the commit message).
  • n/a My commit message includes data points confirming performance improvements (if claimed).
  • My PR is restricted to a single feature or bugfix.
  • n/a White-space or formatting changes outside the feature-related changed lines are in commits of their own.

@codecov
Copy link
Copy Markdown

codecov Bot commented Nov 30, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.58%. Comparing base (808dae2) to head (d223a89).

Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #8792      +/-   ##
===========================================
+ Coverage    80.55%   80.58%   +0.02%     
===========================================
  Files         1707     1707              
  Lines       189051   189055       +4     
  Branches        73       73              
===========================================
+ Hits        152299   152349      +50     
+ Misses       36752    36706      -46     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@tautschnig tautschnig marked this pull request as ready for review November 30, 2025 21:06
cpp_typecheckt::typecheck_expr_trinary uses
implicit_conversion_sequence to compute the common type of the second
and third operands of a C++ ternary expression, then sets the
expression's type to the result and swaps the converted operand back
in. The post-condition was implicitly assumed to be that all three of
expr.type(), expr.op1().type() and expr.op2().type() agree, but that
breaks when bit-fields are involved: implicit_conversion_sequence
strips the c_bit_field wrapper from the requested type during
standard conversion (cpp_typecheck_conversions.cpp:690-692), so the
operand that did not undergo the conversion is left wearing the
original c_bit_field<T,N> type label while the result type is the
underlying T. Downstream code (e.g. simplification of nested
expressions) then trips on the type mismatch.

Fix: after the implicit conversion succeeds, re-align the other
operand to the result type with a typecast_exprt. The outer
implicit_conversion_sequence call has already proved the conversion
is legal, so an unconditional typecast is sufficient and avoids the
silent-failure surface a second implicit_conversion_sequence call
would introduce. Apply this fix-up symmetrically to both branches
(op1 -> op2.type() and op2 -> op1.type()).

Add an INVARIANT at the end of the else block stating that the post-
condition (all three types match) holds, so a future branch added
without the alignment step trips at typecheck time rather than
producing a malformed expression tree that surfaces much later.

Tests: Bitfields_Ternary1 covers the first branch of
typecheck_expr_trinary (op1 is a bool function-call result, op2 is
a c_bit_field<uint8_t,1> bit-field access -- the original diffblue#933
reproducer). Bitfields_Ternary2 covers the symmetric branch via two
cases:

  * Case 1: op1 is a bool literal, op2 is a class with a one-way
    user-defined conversion 'operator bool() const'. The first
    branch's implicit_conversion_sequence(bool -> class) fails;
    the symmetric branch then succeeds via the conversion operator.
    Result types align, so the inner typecast fix-up is a no-op
    here; this case exercises the outer symmetric branch.

  * Case 2: op1 is a c_bit_field<uint8_t,1> bit-field access, op2 is
    a class with 'operator uint8_t() const'. The first branch's
    conversion fails; the symmetric branch converts op2 to op1's
    bit-field type, which strips the wrapper, leaving op1 in the
    bit-field type and the result in the underlying type. The
    symmetric inner typecast fix-up is required to align them.

Fixes: diffblue#933

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
@tautschnig tautschnig force-pushed the fix-933-ternary-cpp branch from 1b4ef10 to 375a747 Compare May 26, 2026 17:52
Copilot AI review requested due to automatic review settings May 26, 2026 17:52
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes C++ conditional (?:) expression typechecking in the C++ frontend to avoid malformed expression trees when operands involve bit-fields, and corrects a typo that prevented the dedicated void/throw-operand handling from triggering.

Changes:

  • Fix void-operand guard in cpp_typecheckt::typecheck_expr_trinary to correctly check both operands (op1 and op2).
  • After successful implicit_conversion_sequence, explicitly typecast the other operand when the computed common type differs (notably when c_bit_field wrappers are stripped), and add an invariant to enforce the post-condition that operand/result types match.
  • Add regression tests covering bitfield ternary conversions and cond ? x : throw ... ternary acceptance.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/cpp/cpp_typecheck_expr.cpp Ensures ternary operand/result type consistency (bitfield wrapper stripping) and fixes void/throw operand guard.
regression/cbmc-cpp/Bitfields_Ternary1/main.cpp Regression test for ternary with bitfield operand requiring implicit conversion to bool.
regression/cbmc-cpp/Bitfields_Ternary1/test.desc Test expectations for Bitfields_Ternary1.
regression/cbmc-cpp/Bitfields_Ternary2/main.cpp Regression test for the symmetric implicit-conversion branch, including a bitfield-triggered alignment case.
regression/cbmc-cpp/Bitfields_Ternary2/test.desc Test expectations for Bitfields_Ternary2.
regression/cbmc-cpp/Ternary_Throw1/main.cpp Regression test ensuring cond ? x : throw 1 typechecks and verifies successfully.
regression/cbmc-cpp/Ternary_Throw1/test.desc Test expectations, including disallowed prior failure messages.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

cpp_typecheckt::typecheck_expr_trinary opens its second operand
handling with a void-operand fast path:

  if(expr.op1().type().id() == ID_empty ||
     expr.op1().type().id() == ID_empty)   // <-- typo: both reference op1

The intent is to enter the block whenever EITHER operand is void
(typically because that operand is a 'throw' expression, which has
type void by [expr.cond] in the C++ standard, or because it is a
call to a function returning void). The disjunction was meant to
catch op1 being void OR op2 being void, but both clauses reference
op1, so a ternary whose third operand is void but whose second
operand is not is silently routed to the regular type-matching paths
and fails with

  "types are incompatible. I got 'signed int' and 'void'."

instead of being typechecked under the throw/void rules. GCC and
Clang accept the construct without warnings.

Fix the second disjunct to read op2(), as originally intended.
clang-format collapses the two-line condition onto one line because
it now fits.

Add a regression test, regression/cbmc-cpp/Ternary_Throw1, that
takes the form 'static_cast<void>(cond ? x : throw 1)' with cond
fixed to true so the throw branch is never executed. The discarded
form (static_cast<void>) avoids generating a goto-program assignment
of the if_exprt, which would otherwise expose a separate
pre-existing issue under the regression suite's
--validate-goto-model profile (the if_exprt's op2 retains its void
type even though expr.type() is set to op1's type, so an assignment
that wraps the ternary fails the lhs-rhs type-equality validation).
That latter issue exists independently on origin/develop for the
mirror case 'cond ? throw : x' and is out of scope for this PR.

The test asserts both that cbmc accepts the construct (^EXIT=0$,
^VERIFICATION SUCCESSFUL$) and that the prior failure modes do not
appear in the output (negative patterns ^CONVERSION ERROR$ and
^.*types are incompatible.*$). Verified by re-introducing the typo
locally that the test fails on all four patterns when the typo is
present.

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
@tautschnig tautschnig force-pushed the fix-933-ternary-cpp branch from 375a747 to d223a89 Compare May 26, 2026 18:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unwarranted "equality without matching types" error

4 participants