Repository navigation
B10: bit-counting and byte-swapping builtins by their definitions, and static_assert with a message - #116
Merged
Conversation
…efined for its argument.
…on check's own place as its focus.
…builtin-argument as definedness.
…ions without a type, as a static_assert message.
…ic_assert with a message.
This was referenced Oct 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Plan item B10 (small frontend gaps), its builtins: a call of a bit-counting or byte-swapping builtin stopped its function, and a
static_assertwith a message crashed cpp-verify.On the base
The example below:
and a function with
static_assert(sizeof(int) == 4, "int is 32 bits");:The crash: the message of
static_assert(c, "text")is a string literal with no type, and the scan for unsupported types in a body (findUnsupportedType) read the type of every expression before checking that it had one.static_assert(c)without a message worked.The other constructs B10 lists were already supported, which this PR checked: character literals,
__builtin_expect, default arguments,if constexpr,consteval, local type aliases,[[likely]], structured bindings of records,return Pt{a, b};, and default member initializers.With this PR
Design, from the references
requires x != 0for__builtin_clz); Verus statesu32::leading_zerosthrough spec functions with axioms.requiresstates it.ASTConverter::convertBitBuiltinlowers a call to its definition over the operand, read as an unsigned machine value of the parameter's width (a mathematical argument in a contract converts to the parameter's type first, as a bitwise operand does):clzandctz: a ladder of conditionals onx & 2^k != 0, with literal results;popcountandparity: a balanced sum of one-bit terms in a byte (the count is at most 64). Summed at the operand's width,parityof apopcounttook 6 s; in a byte it takes 0.8 s;ffs: thectzladder plus one, and 0 for 0;bswap16/32/64: each bit at its new place, combined with|, since no two meet.None of these operations has a check of its own: comparisons with constants, conditionals, sums without carries, and
|. Constant shifts were avoided, since each would add a trivially trueshiftcheck.GCC leaves
__builtin_clz(0)and__builtin_ctz(0)undefined. That is an obligation of a new definedness kind,builtin-argument(archive tag 24, read from schema v6), where the call is evaluated:ite(Require(x != 0), v, v), as constant-table reads require their bounds. In an explicit spec, which is total, there is no check, and they count every bit of 0, as C++20'sstd::countl_zerodoes.Two fixes to E15's places, found here
!g || c; canonicalization folds!!ctoc, so a check in a?:'s else branch stayed at the?:. Every disjunction in a definedness check is a guard the verifier builds, so it is passed into whatever its shape.cv::post(cv::result == __builtin_clz(x))failed at the==. An exit assert now carries its anchor (PassiveExitAssert::Anchor) apart from its condition, which keeps its location as the focus; a focus outside the function, in a lifted helper's body, still leaves the failure at the clause.Mutants
Each mutant below, applied alone and rebuilt, is caught by
bit_builtins.cpp(14 of 14):clzandctzof 0 are not checkedclzof 0 tooclzcounts one fewerctzisffsffsof 0 is the widthpopcountparityis the countbswapkeeps the bytes in place!g || cTests
suite/bit_builtins.cpp(new), in the default encoding and in the integer one:clzof a nonzero argument and its relation to the top bit;ctzll;popcountbounds and zero;bswap32against a formula of shifts;ffsofint;parityagainstpopcount;__builtin_ffs(INT_MIN)in a check;clz(0)andctz(0)(the second in a?:branch, at its call), a postcondition calling__builtin_clz, a lifted helper's__builtin_clzin a postcondition (shown at the clause), and a wrong bound onpopcount;static_assertwith a message;The definitions were also checked against values computed apart from cpp-verify (Python's integers) for 794 calls, every variant at random and edge arguments: all verify, and a wrong value fails. That table takes 103 s, so the test keeps the edges.
On the base the test stops at its first builtin, and the
static_assertcrashes.On a release build of this branch, merged with
mainof today, the whole Verify suite (506 tests) and the unit tests pass.Docs
Functions and loops (the builtins, with the example and its output), limitations, the kinds in tooling, chapter 18 (a builtin's argument), release notes, ARCHITECTURE, DESIGN.