Implementing P3666R4 Bit-precise integers, in libc++

P3666R4 Bit-precise integers was seen in LEWG in Brno last week, and importantly, LEWG approved that std::is_integral_v<_BitInt(N)> is true, and approved the general design direction in §5 of the paper.

_BitInt(N) support in libc++: investigations, possible improvements: looking for guidance points out that we already support _BitInt in the standard library in various places, which is actually a bit of a problem. The proposal ensures that _BitInt is supported basically nowhere (other than type traits), so we’re increasingly diverging from the paper. This also implies that a lot of the currently added support would be non-compliant compared to C++29 _BitInt. LEWG was very clear that they want implementation experience, including tests that make sure you can’t use the standard library with _BitInt.

To solve this, could we add a flag or WANT macro of some kind which would deactivate any _BitInt experimental library support that is not also in the paper? Essentially, this would keep the is_integral, make_unsigned, etc. unchanged, but would remove _BitInt overloads for std::to_chars and such things.

While I could do this on my own personal libc++ fork, there is value to doing this in production: users should be able to use in-development standard version of the feature so that they don’t accidentally use non-portable and experimental functionality that may never be standardized or that may be standardized in a different way. It would also give WG21 a lot more confidence if what is in the paper was widely deployed (even if hidden behind a flag).

1 Like

I’m fine to make further library support for _BitInt opt-in with a macro in LLVM23. But I do believe LEWG is requesting something unreasonable. If it’s proven that library support can be implemented straightforwardly (which seems to be true to me), LEWG should really reconsider this, IMO.

Besides, perhaps we should figure out which part of _BitInt support can be easily implemented in libc++ but not elsewhere.

2 Likes

Jan, Jiang,

Drafted (still AI-assisted, so I might have missed some bits, even if I carefully read and curated the diff) a libc++ PR for what P3666R4’s section 5 asks for: #203841.

How it works: a new _LIBCPP_ENABLE_BITINT_EXTENSIONS macro, off by default. Without it, libc++ refuses _BitInt in byteswap, hash, to_chars/from_chars, gcd/lcm/midpoint, cmp_*, saturation arithmetic, and format. Define the macro and you get the experimental support libc++ has been shipping. Type traits and numeric_limits still recognize _BitInt either way; those stay per the paper.

The implementation is small: one trait edit covers most of the surface (changing __signed_integer/__unsigned_integer propagates through <bit>, format, cmp, saturation, mdspan). The rest is one-or-two-line gates per function. Two new tests cover both modes.

On Jiang’s “this is straightforward” point: yeah, the PR backs that up. Five spec sections handled by one trait edit. Whether LEWG wants to revisit that is their call, but if anyone needs implementation evidence later, this PR is it.

What’s NOT gated yet (intentional follow-ups, smaller PRs): <atomic>, <random>, <chrono>'s duration, <iterator>, <mdspan>'s slice helpers, std::complex integral overloads, std::byte shifts, the _n algorithms, simd, ckd_*.

Side finding worth flagging: __has_extension(bit_int) returns 0 in trunk Clang. No entry in Features.def. Every existing libcxx test gating _BitInt blocks on TEST_HAS_EXTENSION(bit_int) is silently dead code. New tests use defined(__BITINT_MAXWIDTH__). A one-line Clang patch would fix the broader issue.

On “what’s easy in libc++ but harder elsewhere”: the cascade trick depends on libc++ centralizing integer recognition in one trait. STLs that don’t may need to gate at each call site instead.

PR: https://github.com/llvm/llvm-project/pull/203841

Xavier

1 Like

The concerns are that

  • the testing matrix explodes, even if the implementation is straight-forward
  • huge _BitInt cannot be efficiently passed by value, so maybe some ABI-altering attributes would be needed, or maybe a signature taking _BitInt by const& would be better
  • some overload sets would be created, like between a template for _BitInt and concrete overloads for other integers in std::to_chars

I think the conservative approach to start with a blank slate and add the library support piece by piece makes sense. Otherwise P3666 would just take forever to review.

That would mostly apply to numerics where we have intrinsic support in C++ mode and where GCC is lacking. For example, @llvm.clmul, @llvm.pext (soon) for std::clmul and std::bit_compress and such things. We have lots of __builtin_elementwise intrinsics in Claang that make both scalar and <simd> implementations very easy.

If other libc++ folks are fine with it, this is ideal (that is, making experimental _BitInt support opt-in) and making the P3666/C++29 minimal support the default. The question is whether we’re fine with breaking the code of the small amount of users who are calling standard library functions with _BitInt and expecting that to work already, but I guess those users would have a rude awakening anyway if they ever wanted to switch to -std=c++29 -pedantic-errors at some point and nothing compiles.

the testing matrix explodes, even if the implementation is straight-forward

This is not necessarily a big issue; for this PR, I see around 70-80 static_assert lines per facility, lifted into one ~150-line verify file. For the ~7 facilities currently gated this fits in one test file; per-facility positive tests grow by ~10-20 lines. The point is that libc++'s test side scales linearly here.

some overload sets would be created, like between a template for _BitInt and concrete overloads for other integers in std::to_chars

For std::to_chars, we use is_integral<T>::value SFINAE on the template; the gate adds one extra __admits_bitint_extension_v constraint on top. No new template-vs-concrete-overload tie-break.

I’m not challenging LEWG direction; just that for libc++ I do believe the concerns are not that high. On the other hand I’m anything but an expert, so take this with a bag of salt.

Edit: We dont need the feature flag below

By the way: a small follow-up (also AI-drafted, curated) regarding the bogus feature flag: opened a Clang PR registering the missing Features.def entry for __has_extension(bit_int). [Clang] Register __has_extension(bit_int) and __has_feature(bit_int) by xroche · Pull Request #203871 · llvm/llvm-project · GitHub

Both __has_extension(bit_int) and __has_feature(bit_int) (true in C23+) are registered. C++29 can extend the __has_feature predicate once a LangOpts.CPlusPlus29 lands.

Side effect of merging: the ~42 libcxx test sites probing TEST_HAS_EXTENSION(bit_int) (popcount, byteswap, etc.) will activate. PR #203841 already gates those files via ADDITIONAL_COMPILE_FLAGS, so activation is benign; the previously-dead _BitInt blocks compile and pass.

Xavier

1 Like

I’m very much against this. This feels more like people don’t want _BitInt to be in the standard, so they throw arguments against it until one sticks (though, granted, I haven’t been in discussions about this for the most part, so I don’t want to say that’s what’s actually happening).

Either way, regarding the main premise of this RFC: How exactly would libc++ be non-conforming with the paper? [conforming] gives quite explicit permission for implementations to provide additional overloads.

More generally, currently this is a conforming extension - why would LEWG want to prohibit us from providing a (IMO) very sensible extension? The only thing that can happen here AFAICT is that users have a worse experience.

Regarding the objections (I realize they are probably not yours, so I don’t necessarily expect answers from you here):

So the C++ should never have added templates? Or, for that matter, where was that concern when std::simd was added? How is _BitInt harder to test than std::simd?

Is _BitInt(N) vs const _BitInt(N)& even observable? If not, why is that relevant for standardization?

Libc++ doesn’t implement this quite correctly actually, though I’ve yet to see someone complain about it. Would it maybe make more sense to allow implementations to constrain this generally on is_integral instead of requiring a silly amount of overloads that provide basically no benefit?

3 Likes

I’d also be curious whether any of these concerns come from actual implementers.

3 Likes

One industry-user data point. I don’t have a channel into WG21 and this thread is the closest thing, so I’ll use it :person_shrugging:

I work at Algolia on the search engine. We needed a 256-bit integer for hash and rank arithmetic on hot paths. We first proposed __int256_t as a Clang builtin (PR #182733, since abandoned). When _BitInt landed and covered 256 bits as a language feature, we dropped that proposal and switched to _BitInt. What made _BitInt(256) actually usable in our codebase was not the language feature on its own. It was libc++'s existing extensions: to_chars, hash, byteswap, cmp_*. Without those we would have written the glue ourselves, and _BitInt would have stayed a second-class integer for any real codebase.

P3666R4 section 5 asks libc++ to remove that glue. From a consumer side it lands as “the language has a 256-bit integer but the standard library refuses to talk to it.” That removes most of the reason we adopted it. If the testing, ABI, and overload-set concerns aren’t coming from libc++ implementers themselves (the question in post #7), I would also read [conforming] the way philnik does and keep the extensions as a conforming extension. The paper’s restrictions can apply to the parts of _BitInt that genuinely need spec work.

I have the feeling that there is a continuous disconnection between industry and WG21. The same kind of disconnection that makes new[] unable to be reallocated, leading the industry to re-implement std::vector to get benefits from modern hardware.

_BitInt should be a first-class citizen, but that’s my very industry-oriented point of view.

2 Likes

It’s not conforming if this alters the behavior of a valid C++ program, and a program that does requires(unsigned _BitInt(1) x) { std::popcount(x) } expects the expression to be false. Added overloads should not change that. You can add additional overloads for compiler extensions that are not part of any valid C++ program, but _BitInt would be a standard feature rather than a compiler extension in C++29, so you cannot change the overload set to accept it.

This applies even more to where _BitInt does not fail the Contraints of a function but the Mandates. A conforming implementation has to emit a diagnostic when Mandates is violated, so if you write a static_assert that lets _BitInt pass when it shouldn’t, that’s not conforming. You also wouldn’t be adding extra overloads in that case anyway, but modifying static_asserts in non-conforming ways.

Because _BitInt extensions in the standard library steal design space from the committee. If anyone can extend the standard library to support _BitInt in various places, you cannot add overloads to the standard without potentially breaking user code.

:person_shrugging: I don’t think it’s harder to test, but to be fair, it does add a requirement to test basically any integer feature for various _BitInt sizes as well. If you wanted to be really thorough instead of just adding like 3 or 4 relevant sizes, that would add quite a lot of tests.

It’s not observable, but without ABI-altering attributes, _BitInt is currently passed on the stack for huge sizes, and that can get much more expensive than passing around references to it. It is quality of implementation, but I don’t think LEWG understands that fully. To be fair, a user doesn’t just get to magically write a function that takes _BitInt by value but passes it indirectly with no copies either; only the standard library gets to have an arbitrary altered ABI in spite of the standard wording.

Perhaps yes, but that’s a question for a different proposal.

My impression is that they come only from people who are not library maintainers at least. However, there are definitely people who have worked on standard libraries and who have raised those concerns in LEWG. Can’t really disclose more without privacy concerns.

I generally share the Nikolas’ position (from Clang peanut gallery, of course).

You are not wrong, but I find this to be an extremely pedantic point of view that I believe will not be supported by the Committee itself were you to raise this as a compatibility concern against a paper, because otherwise we’d have to entirely freeze the Standard. So this is not a particularly convincing argument to bring up.

That is, if the overloads you add do something different than what the extension do. Are there many non-obvious choices to be made? If libc++ does something obviously stupid in its library support for _BitInt, I’d expect maintainers to be interested in fixes for that. If libc++ picked a particular option in a non-obvious choice and was able to ship it without users complaining, I believe that’s an input for LEWG.

3 Likes

Sadly, I missed both the Croydon and the Brno discussions on this.

From the WG21 “library design” perspective, I believe there are two sensible ways of treating _BitInt. Either we support it in the core language but we don’t make it is_integral and we don’t support it anywhere in the library, or we actually support it properly in the library (is_integral<_BitInt> == true and we make it work wherever feasible). The latter requires a lot more work, but it’s actually useful for people. That’s also what libc++ is already trying to approximate.

TBH, I don’t really understand WG21’s decision in Brno. If we wanted to prevent closing design space in the library, I would have made _BitInt a pure core language type with no library integration whatsoever, and then I would have left is_integral == false. I’ll have to chat more with people that were part of the discussion to understand beyond what the minutes say.

Either way, nothing has been voted for now, so for the time being libc++ is still just providing a conforming extension. I would strongly suggest that we don’t change anything at all in libc++ for now. Changing our behavior will only:

  1. Break existing users
  2. Change the status quo on which WG21 discussions have been happening, confusing everyone and ourselves
  3. Create a flip-flop possibility if WG21’s decision changes again

Instead, I’d wait for the WG21 discussion to truly settle down before assuming what WG21 is going to land on. If the Brno decision ends up sticking for C++29, then I would remove libc++'s _BitInt extension (that’s going to suck but we can do it over 1-2 releases) and implement exactly what the Standard says – anything else would just create more confusion.

So, TLDR: let’s not change a thing in libc++ for now. Let’s pretend the Brno discussion never happened, and let’s try to understand why they want to prevent libraries from “properly” supporting _BitInt. Then either WG21 will change their mind, or we’ll understand why their design makes sense, or we’ll reluctantly implement their design without understanding it – those are the three choices.

2 Likes

LEWG has voted in Brno to make is_integral_v<_BitInt(N)> , true, that’s the direction of the paper, and that’s what libc++ currently does, so we are all on the same page.

That’s what they were going for in Croydon, but that idea was overturned, and it was not really a good idea anyway. The paper discusses why extensively, and we spent 2 hours in a joint LEWG/EWG issue on this issue in Brno.

All the relevant votes have taken place. The only thing that’s missing is implementation experience, which requires minor adjustments to libc++ such as the proposed feature flag, and then this thing can be put into the standard. All the contentious points have been moved out of the way, and the general design direction in §5 of the paper has approval.

I think you should check the latest poll results in P3666 R4 Bit-precise integers · Issue #2420 · cplusplus/papers. It seems like your view of the situations is still based on the Croydon state of things from March this year.

My 15 cents here.

I should restate again that I’m not a WG21 regular, so parts of this process are rather opaque to me :person_shrugging:

I read the poll record in cplusplus/papers#2420, and there’s one thing I can’t square. The decision to prevent library support comes from the Croydon poll: about 20 voters in a room of 25. As far as I can tell it wasn’t revisited at Brno, even though Brno did re-poll is_integral at a much larger table (room of 65). So is the “prevent library support” direction considered as settled as the trait, or is it the part still most open to implementation experience? I honestly don’t know how to read that, and I’d welcome a correction if I’m misreading how the room sizes and the consensus calls relate.

This matters to me for a concrete reason. We’s like to use _BitInt(256) for hash and ranking arithmetic, and the only reason it would be usable today is libc++'s existing extension: to_chars, hash, byteswap, the traits. If the missing piece for standardization is implementation experience, that experience exists, it’s in production (on hand-crafted big integers for now). So what I’d like to understand is how deployed field experience gets weighed against a design poll, and what the right channel is for putting an industry use case on the record. I get that a forum thread isn’t an official one, so please advise me where this should go.

[ On direction, I defer to the libc++ maintainers on what happens to the patch(es). ]

Yes, I understand that, but my understanding is that they also want to prevent us from providing extensions in the rest of the library for handling _BitInt in e.g. <bit> & friends. That means removing part of our existing extensions, and that’s overreaching IMO.

Either they say “we don’t support _BitInt in the library”, and then is_integral == false, or they do the actual work of figuring out what needs to change in the library to integrate it properly. Asking that we remove a useful extension to “preserve future WG21 design freedom in the library” is trying to ship an incomplete feature.

If no implementation was shipping a _BitInt extension, the reasoning would still be the same but it would be a lot less vexing. In the current state of things, I feel like they need a good rationale to ask one of the implementations to break users and undo work that’s already done. My understanding is that the rationale for the Brno vote is basically that there are non-trivial considerations WG21 doesn’t know how to solve yet. In that case, why not let _BitInt be a pure extension until we have clarity? Compilers already provide _BitInt anyway, so there’s no rush for the ISO Standard to actually add it, especially if that means decreasing the effective level of functionality that users get.

P3666R4’s section 5 mentions:

  1. explosion of the test matrix
  2. design problems in P3666R3 (basically unresolved open questions about how to handle this or that)
  3. tackling all the library integration in one paper is too much work

Did I miss anything? The first concern seems very weak, especially if libc++ is already biting that bullet. (2) and (3) are basically what I said above: WG21 knows they want library integration, but it’s not fully designed yet.

I hope this thread can serve as evidence that the vote was taken with incomplete information. In particular, the only existing implementation experience is libc++, and it diverges from what you folks argued for and what WG21 voted on. And now you’re asking for libc++ to break one of its extensions so that it can provide a precedent for a decision that it disagrees with (based on this thread). That’s not a great proposition.

If we come to an understanding of WG21’s position, or if they plenary-vote it into C++29, then we’ll conform. But in the current state of things, there’s no reason for libc++ to ship a breaking change with the sole purpose of endorsing a non-plenary WG21 vote that it disagrees with.

To be clear, I’m 100% open to discussion and understanding your point of view, and I think we all are. But let’s approach it as a discussion to reach a common understanding and goal instead of pretending that libc++ can be strong armed into making changes due to a LEWG vote – that’s not how it works.

This is all up to the chairs of the room, the people who speak up and what they say. In this case I suspect that having someone from libc++ in the room might have changed the discussion a bit. But also, this thread itself is part of the overall process IMO: prior to this discussion, I would have been much more neutral on my position about _BitInt, cause I didn’t know that others felt strongly like I do, and I didn’t have as much anecdotal evidence of why our _BitInt extension was useful. So to some extent, I view this as all part of the process.

On that note, this thread isn’t really the right place to discuss WG21 decision making processes (but discussing the decisions themselves is fine, of course). In this case, here’s what I think should happen:

  1. Libc++ will not change its _BitInt extension
  2. P3666 will not get implementation experience to support Section 5
  3. P3666 will be seen again by LEWG in Rio (that was already the plan)
  4. LEWG will decide what they want to do based on the additional information they’ll now have

In practice, I believe that a reasonable path forward would be to leave is_integral == true without explicitly disallowing implementations to support _BitInt elsewhere, and then to add library support incrementally. My preference would be to do an initial survey of the library (at the minimum based on what libc++ already provides today) and use that as the starting point for library integration, understanding it’s not perfect. But that’s all up to LEWG.

3 Likes

Then why have that paragraph at all? We’re already allowed to do that without it.

[intro.compliance.general] actually gives us express permission to accept those.

1 Like

One more datapoint, I hope I am not causing too much noise.

I extracted all recent _BitInt std support PRs and had a candid look at the numbers. I crunched the stats with automated scripts and AI-assisted collected datapoint, which I checked.

The Tl;Dr is at th every end. Feedback welcome.

What libc++ provides for _BitInt today

A factual snapshot for the P3666R4 discussion and the LEWG Rio review.

Disclosure: the author favors making _BitInt a first-class library type and uses it in production. This document reports what libc++ ships, what it cost to build, and what is left. It is a datapoint, not a position. The cost figures are checked against two independent sources, the merged Git commits and GitHub’s own line counts, which agree exactly.

Background in one paragraph

_BitInt(N) is an integer of arbitrary, fixed width, for example _BitInt(256). It exists in C23 and in Clang today. P3666R4 proposes to standardize it for C++29, but its section 5 would keep it out of most of the standard library by constraining facilities to reject it. libc++ already supports _BitInt in several of those facilities as an extension. This surveys that existing support, so the discussion can start from what exists rather than from estimates.

What works in libc++ today

A meaningful part of _BitInt usability predates the recent work. The compiler and baseline libc++ already provided it, at no cost to the three PRs below. Separating the two matters for reading the cost figures: the PRs bought the second table, not the first.

Predated the recent work (compiler builtins and baseline libc++):

Facility _BitInt support Why it already worked
is_integral, is_signed, is_unsigned, make_signed/make_unsigned All widths Compiler builtins; same path as __int128
Arithmetic, comparison, and conversion operators All widths Language feature
numeric_limits: is_specialized, min, max All widths Already specialized
to_chars / from_chars Up to 128 bits Gated on public is_integral, already true
byteswap acceptance Up to 128 bits, byte-aligned Gated on std::integral, already true

Added or fixed by the three PRs:

Facility Change Source
<bit>: popcount, countl_zero/one, countr_zero/one, bit_width, bit_ceil, bit_floor, rotl, rotr, has_single_bit Enabled (were constrained out) #185027 trait cascade
cmp_equal/cmp_less/… and in_range Enabled #185027 trait cascade
Saturation arithmetic (add_sat, sub_sat, …) Enabled #185027 trait cascade
mdspan extents (as the index type) Enabled #185027 trait cascade
format Enabled, up to 128 bits #185027 trait cascade
numeric_limits: digits, digits10 Fixed for non-byte-aligned widths #193002
byteswap Added padding-bit rejection (correctness) #196512

What it cost to build

Three merged pull requests deliver everything above.

PR Delivered Implementation Tests
#185027 the trait cascade, reaching 5 facility areas 24 lines, 1 file 1327 lines, 15 files
#193002 the numeric_limits fix 11 lines, 1 file 138 lines, 4 files
#196512 byteswap 24 lines, 1 file 209 lines, 4 files
Total about 8 facility areas 59 lines, 3 files 1674 lines

Three things stand out. The implementation is small: 59 lines across three header files, each a constraint or trait adjustment rather than new machinery. The cost is almost entirely tests, about 96 percent of the changed lines. And a single 24-line trait change reached five facilities at once with no per-facility implementation. The tests cover representative widths such as 8, 13, 48, 64, 128, and 256, signed and unsigned, rather than every possible N.

One clarification on what these numbers do and do not capture. They are the final merged code, not the effort. The elapsed time and the number of review rounds ran higher than the work itself, because the author was new to libc++. A good share of the review went to local conventions rather than to _BitInt: where tests belong, formatting, and a feature probe that silently disabled some tests until a reviewer caught it. An experienced libc++ contributor would reach the same result with less back-and-forth. The intrinsic cost is the code above; the rest was onboarding.

What is not done yet

Needs tests only, because the trait cascade already makes the code work: gcd, lcm, midpoint, and std::abs (a pull request is open). Roughly one test file each.

Needs genuinely new code: to_chars, from_chars, and format for widths above 128 bits. These need arbitrary-precision decimal conversion, which the library does not have today. The work is real but bounded, and one decimal core serves all three.

Deferred on purpose: std::hash. Its output becomes a permanent compatibility contract once shipped, so it should wait for more experience.

Out of scope here: std::atomic and std::simd, which are separate and larger efforts, and _BitInt wider than 128 bits on non-x86 targets, which is a compiler back-end limit rather than a library one.

Honest caveats

  • Tests do dominate the cost. The concern that supporting _BitInt enlarges the test surface is correct. The qualifier is that the per-facility cost is small and bounded, and one trait change cascades to many facilities.
  • Supporting a facility is an ongoing maintenance commitment, not a one-time cost.
  • Wide-value charconv and format is genuine new work, and it is x86-only today.
  • Clang documents the _BitInt ABI as not yet stabilized. That is a reason for caution that is independent of library cost.
  • This is one implementation and largely one contributor. libstdc++ and the MSVC library may see different costs.

Bottom line

Most of first-class _BitInt library support already exists in libc++, and it was cheap to build, because one trait change cascades and the rest is tests. The single piece that needs real new code is arbitrary-precision decimal conversion for values wider than 128 bits, and it is bounded. The remaining items are deferred by choice or out of scope. Whatever the committee decides, this is the ground truth of what shipping it actually took.

I don’t think it was. The room was well-aware that libc++ is already going ahead and providing _BitInt support in various places.

The key problem isn’t that there is one position. People roughly fall into three parks:

  1. We want fully-fledged _BitInt support and add that into the standard library right now, in this paper.
  2. We want fully-fledged support, but not in this paper. It’s too much to review and implement all at once.
  3. We don’t want _BitInt support in the standard library because it sucks and it’s lame. Maybe if we had something better with fewer implicit conversions and a nicer spelling, we would give it support.

The only viable compromise that can make some progress in WG21 is to take the second position for now because it actually makes everyone happy. The people who want support right now just have to be patient, and the people who think _BitInt sucks will be pleased that we’re not adding it throughout the standard library.

Well, that’s exactly my point and my approach. The only problem is that libc++ running away and shipping library support for a feature that’s in the middle of standardization, knowing full well that this could break in the near future when the standard version takes the place of those things. I don’t see a path forward where the committee is forced into cramming an ever-increasing degree of _BitInt support into the standard’s numeric facilities just because one major implementation has to eagerly add that support. Is the expectation that immediately when any libc++ PR is merged that adds _BitInt support to something, I have to edit that feature into P3666, otherwise we’re breaking user code that’s already shipped? That seems to be putting the cart before the horse.

In any case, my original suggestion wasn’t to have an opt-out in flag anyway, specifically because an opt-in flag would break the _BitInt support that’s already provided. It would be very helpful if you had to explicitly opt in while using -pedantic-errors/-Wpedantic, or if you had the ability to provide a flag to disable the experimental libc++ _BitInt support so you don’t accidentally use something that may change through future standardization.

That seems like a pretty reasonable option, doesn’t it?

You’re allowed to do whatever you want as long as _BitInt is not a standard feature because you can treat it like a compiler extension, and as long as you emit a diagnostic, any use of a compiler extension makes the program ill-formed and gives you the freedom to do whatever you want.

However, that freedom is immediately about to be taken away once _BitInt stops being a compiler extension. Then you’re just providing overloads and static_assert behavior that are non-compliant. Thus, for all intents and purposes, you should not treat it like a compiler extennsion because it soon might not be.

That’s exactly my point. If your interpretation was correct I wouldn’t need that paragraph, because it would already allowed through other parts of the standard.

I’d really like your answer to my second part of the comment, as that directly contradicts at least the static_assert part. And I’m also not convinced that we’re not allowed to extend constraints, though that seems much less clear.

Once _BitInt is not a compiler extension, I don’t see how the implementation can arbitrarily not trigger a Mandates/static_assert despite the standard saying it should. As long as _BitInt is a compiler extension, you can get away with changing Constraints and Mandates as much as you want.

[intro.compliance] only applies if it’s actually a compiler extension, so that should answer the second part of your question.

What is “an actual compiler extension”? Is it OK if the compiler makes it work magically but not if it’s implemented in the header of the library implementation? Surely not?
[description] says that a Mandates renders the program ill-formed, and [intro.compliance.general] says implementations are allowed to accept ill-formed programs iff they issue a diagnostic. I don’t see how you could read from this that we aren’t allowed to accept programs violating Mandates. And I don’t see any wording saying [intro.compliance.general] only applies to the core wording part of the standard.