A lot of developers still talk about unsigned sizes as if they were the responsible choice. Lengths cannot be negative, array indexes cannot be negative, so the type should say so. Clean. Safe. Done.
The problem is that computers do not execute slogans. They execute arithmetic rules. And once unsigned becomes the default type for sizes and indexes, those rules start leaking into everything around them: loop conditions, modulo math, promotions, casts, pointer offsets, and every small helper function that needs to move one slot left before it moves two slots right.
A small post from the C3 language project hit r/programming this week for exactly that reason. The headline was about C3 switching to signed sizes by default. The broader story is what happens when one default type choice forces an entire language to spend years inventing workarounds for bugs it created itself.
What is actually verified
On May 2, C3 maintainer Christoffer Lernö published a short blog post explaining why the language is moving away from unsigned sizes. The post does not present this as a cosmetic preference. It argues that several familiar bug patterns all trace back to the same earlier decision: making unsigned the normal type for sizes and lengths.
The examples are concrete.
One is the classic countdown loop that never terminates because an unsigned value can never become less than zero. Another is mixed signed and unsigned comparison logic that looks harmless in source code but changes meaning after integer promotion. The post then lands on the nastier case: % and / behavior when a large unsigned value gets mixed with a signed operand.
That edge case is not hypothetical. In January, a C3 issue titled mixing unsigned integers with signed integer using the % produces potentially undesirable values documented a minimal reproducer where a large ulong modulo int produced a negative result. A maintainer comment on the issue called out the larger problem directly: the language had tried to make mixed signed and unsigned math ergonomic because usz was used everywhere, but that convenience left a class of surprising results hiding in normal-looking expressions.
There is also public evidence that this is not just a blog-post thought experiment. In April, pull request #3091, titled Use isz instead of usz., was merged into the C3 compiler repository. The code change is large, and the blog post makes clear why. Once sizes are unsigned by default, you do not just pick a type. You commit the whole standard library and compiler pipeline to living with its arithmetic behavior.
The part people miss
The usual defense of unsigned sizes sounds intuitive: if a size cannot be negative, why allow negative numbers at all?
Because most size-related code is not about storing a final length. It is about computing with lengths.
You subtract one index from another. You walk backward through a buffer. You wrap offsets in a ring. You compare a counter against a boundary after a transformation. In those cases, signed arithmetic often fails louder and sooner. Unsigned arithmetic often gives you something worse: a plausible number that is wrong.
That is the core of Lernö's argument, and it is stronger than the language-war version of the debate. He is not saying unsigned integers should disappear. He is saying they are a bad default for the kind of arithmetic programmers do all day around containers and memory layouts.
The C3 issue thread backs that up in a more practical way than the blog itself. One commenter demonstrated a case where the wrong sign on a remainder could cascade into array lookup failures. Lernö's own follow-up comment is the most revealing line in the whole paper trail: explicit casts sound safer in theory, but once a language forces them everywhere, developers stop reading them as warnings and start treating them as typing tax.
That should sound familiar to anyone who has worked in C-family systems code, Rust included. Once the default choice forces regular back-and-forth casting between "the size type" and "the arithmetic type," the language starts teaching users to normalize friction instead of noticing danger.
Why the reaction matters
The Hacker News discussion was not unanimous, which is a good sign. If this were just a tidy maintainer announcement, nobody would bother arguing.
One camp made the classic low-level case for unsigned: hardware work, narrow word sizes, and situations where you really do want the full positive range of a machine word. That is real. Another camp pushed the opposite view just as hard: most programs will never index anything close to 2^64, while they will absolutely subtract indices, wrap counters, or trip over silent underflow.
The Reddit thread on r/programming landed in a similar place. Several comments focused less on C3 itself and more on the fact that Rust developers already know the cast pressure this creates. Others zeroed in on the ring-buffer and modulo examples because they are the kind of bugs that look fine in code review until they do not.
The reaction is what makes the story worth posting. The headline is about one language. The argument goes beyond that one language and asks whether type systems should optimize for representational purity or for the arithmetic developers actually write.
What remains uncertain
This is still one maintainer's case, not a controlled study across languages.
C3 is also a small enough ecosystem that it can make a disruptive change more easily than C, C++, or Rust can. That matters. It is much easier to admit a default was wrong when you are not carrying decades of ABI and library expectations.
There is also a real counterargument around niche low-level domains. If you are working with smaller integer widths, tight memory layouts, or deliberately modular arithmetic, unsigned types are not a bug. They are the point. The C3 post does not refute that. It argues that those cases are specialized enough that they should stay explicit.
That distinction is persuasive, but it is still a judgment call.
What developers should take from this
The useful lesson is not "signed good, unsigned bad." It is that default numeric types create cultural habits.
If the default size type makes ordinary arithmetic harder to express safely, the language will either drown in casts or build more and more special rules to make the casts disappear. C3 seems to have discovered the same thing many projects eventually do: once you spend enough time smoothing over unsigned footguns, you are no longer defending a clean model. You are paying interest on an old type decision.
It matters beyond C3. The real bug was not one modulo edge case. The real bug was treating "cannot be negative" as more important than "will be used in subtraction tomorrow."
Sources
- C3 blog, "Unsigned sizes: a five year mistake" (May 2, 2026): https://c3-lang.org/blog/unsigned-sizes-a-five-year-mistake/
- C3 compiler issue #2807,
mixing unsigned integers with signed integer using the % produces potentially undesirable values: https://github.com/c3lang/c3c/issues/2807 - C3 compiler pull request #3091,
Use isz instead of usz.: https://github.com/c3lang/c3c/pull/3091 - Reddit discussion on r/programming: https://old.reddit.com/r/programming/comments/1t20rdz/unsigned_sizes_a_five_year_mistake/
- Hacker News discussion: https://news.ycombinator.com/item?id=47989154