I have known about this for quite some time, and the more I think about it, the more useless it seems. Sure, the underlying machine operation is just addition, which is indeed commutative, but at the type system level, the pointer and the offset have distinct roles. It just makes no sense to allow to commute them, just like it makes no sense to allow to commute arguments to, say, strchr, just because the compiler can figure it out by looking at the types. When was the last time you had a practical reason to write "offset + pointer" or "index[array]"?
In most languages, the indexing operator is not commutative. In Rust, pointer offseting is expressed as a function call or a method, also not commutative. I have never seen a single complaint about either. It's not something people want or care about, it's just a tedious detail.
> It's not something people want or care about, it's just a tedious detail.
It’s not something idiomatic, but indexing in C is syntactic sugar. Not sure why they allow it in the syntax, but forgetting that arrays are pointers and not special type is just asking for bugs.
Arrays are not pointers in c. Though they are very similar and will implicitly convert, there are differences. The big and obvious difference is that declaring an array will allocate space for it. In a function, on the stack. In a struct, inline. They have different sizeof. I think there are also some other stuff I can't remember.
In C there's only two operations you can do with arrays that do not decay to a pointer: `sizeof(array_var)` and `&array_var`.
The latter produces a rarely-seen "pointer to array", i.e. a type like `int (*)[10]` (pointer-to-array syntax works like pointer-to-function syntax).
I don't mind adding new features in committees but if they are going to remove compatibility, they should rename their language to nuC or CantbelieveitsnotRust(yet).
No offense intended to Rust which is a fine language.
If C was not supposed to be changed to correct its past mistakes, we would still increment with a=+1 instead of a+=1, or we would use something called nunununuC.
> C11 is last C as far as I'm concerned.
Are you sure? I think C23 brought so many enhancements it's a pity to ignore it.
There is such a thing as legacy code. I don't know why they were writing K&R style definitions in what must have been the late nineties but they were. It was standard C after all.
Yesterday, I watch a quick video[0] where Matthew Butterick was comparing book sizes and their appeal. “The C Programming Language” was my second programming book (after one about JavaScript 1.x) and I still remember it fondly. Easy to start with (with CodeBlocks on Windows and gcc on Linux) and the concepts were nicely explained. The book were also very nice.
Isn't this easily catchable with static analysis, -Werror -Wall? I've never had a practical problem with this.
Also arrays aren't pointers in C, they decay into pointers. You can see this since sizeof will work differently in the function that instantiates the array versus one that takes in the pointer as a parameter.
Lisp minimalism is very different. It assumes a runtime with automatic memory management for example, even if the language concepts themselves are minimal, especially in Scheme, it has almost no syntax and just a few core facilities on top of which everything else is built, which are closely related to the Lambda calculus, kind of ignoring completely what real computers actually look like.
In C the minimalism comes from providing only the minimal set of things that are available in most (maybe all) architectures (the Von Neumann paradigm), like linear memory, a simple function calling convention, close mapping to Assembly operations etc.
But C syntax is not very minimalist compared to Lisp , let alone Forth. The fact that C syntax became prevalent in the programming world seems to be mostly an accident to me, it’s not objectively better than those minimalist languages’ or Pascal’s, Prolog, ML families.
That is because C already worked everywhere even before CPU makers agreed on things like making pointers and words the same size.
Edit: And by "worked everywhere" I mean C did the heavy lifting of being a programming language that was even a little bit portable across architectures.
Instructions like AAA (Ascii Adjust for Addition) on the 8088. The 8087's stack architecture. XLAT, STOS, etc. Asymmetries in how the registers work. ENTER/LEAVE. Kludge instruction sets like the Z80.
An instruction set ideally suited for C is the PDP-11.
Why would AAA be an instruction made for C? I don't see this. Or 8087's stack architecture? Or xlat? ENTER/LEAVE is also a counter example where CPU vendors added instructions for other languages than C.
Brainfuck has a even more minimal common denominator. It's just increment pointer, jump to address, read/write. It's basically just a minimal turing machine that ANY architecture will run.
Again: you're defining minimum wrong here! It's not minimum in the sense of using one instruction (which is possible, see "mov"). A minimum common denominator is NOT that, see https://en.wikipedia.org/wiki/Lowest_common_denominator (I am not a native speaker, apparently it's normally said "Lowest Common Denominator" in English, in my language it's "mínimo múltiplo comum", so sorry for the confusion).
For example, the LCD of 4 and 3 is 12, that's the "minimum" number they have as a common multiplicator.... it's not 0, even though both multiplied by 0 equal 0. In more concrete terms for C: C is more like the maximum number of things that intersect across all different "common architectures". You want to have most things that everyone supports, not the lowest number of things possible! Hope this clarifies what I mean.
> In C the minimalism comes from providing only the minimal set of things that are available in most (maybe all) architectures (the Von Neumann paradigm), like linear memory, a simple function calling convention, close mapping to Assembly operations etc.
This is beautifully illustrated in the old classic The C Companion by Allen Holub which overviews a simple abstract demo architecture and code generated for it. This is a small book (in the vein of K&R C) but contains enough illuminating info. for a programmer.
I would add on top of brabel's post that C is surprisingly difficult to parse correctly (C++ notoriously so).
Zoom out a little: C is a lot like Unix: simple probably isn't the right word; `underengineered` comes to mind. Which leads to complexity, as you need to make things work in the real world. And so Unix syscalls being designed in the early 1970's for a PDP-11 don't really map to modern needs. And so every unanalyzed complex C app has memory leaks.
To be clear, I like C, I like C++, I like Objective-C, I like Swift; I'm comfortable in all of them. But, in 2026, I don't see why for native code everyone shouldn't be programming in Rust / Swift / other modern memory-safe flavor for any new production use. Zig if you want faster compile times, I guess.
It is "underengineered" perhaps if you believe it is the job of the language to prevent memory leaks (which Rust also doesn't). IMHO it is just fine - and in fact better - if the language is small and analysis is done by a separate tool.
I also do not see why the simplicity of C should lead to complexity. I have build complex systems in C. I did this using C++ in the past, but switched to C because I found out that all the complexity of C++ was a huge distraction and not something that helps me design better systems. C++ also came with a lot of claims that all the features are absolutely needed to build good abstractions and that this is not possible in C, something I found to be completely false. (And then, a lot of complex and still extremely reliable software I use daily is build in C. My practical experience completely contradicts the myth propagated nowadays by some that all C is inherently bad and unreliable . In my experience it is the exact opposite: The C software I use is in fact the most reliable.)
My view is that Linux and free software is still going strong, because it managed to largely keep out the unnecessary complexity out of the core infrastructure. With Rust and AI, I worry that now get a lot of overly complex infrastructure and that it will be hard to ever get this complexity under control again.
> But, in 2026, I don't see why for native code everyone shouldn't be programming in Rust / Swift / other modern memory-safe flavor for any new production use. Zig if you want faster compile times, I guess.
Because C is very simple (unlike Rust) and works everywhere. Zig is not yet stable. Go and Swift are under the governance of tech companies.
The true appeal of C for me is the standard and how it applies only to the language. You can easily take a project from 2 decades ago and port it to a current platform. Lot of current ecosystem is way too fussy about tooling to do this.
> Because C is very simple (unlike Rust) and works everywhere
That's definitely true. C is a "Worse is better" language and as such it's everywhere. You can knock together a halfway usable C for some crap hardware in a few weeks and then the hardware is saleable because there's a C implementation.
> You can easily take a project from 2 decades ago and port it to a current platform.
Much of the software I wrote two decades ago in C won't even build today. Good luck figuring out why, periodically I try to figure out which weird 2000s era Mac OS hacks don't like a 2026 Linux system and eventually I give up and write modern software instead.
In theory C written in 2006 definitely "just works" on a 2026 Linux machine but in practice real world C is broken and even though I (co)wrote it I don't know why. Also in practice the first serious Rust project I wrote in 2021 I just found it, checked out the oldest working version ("first rough working code" says the log) from git, cargo run, works as expected.
Maybe it's because the C was four times older, but I think it's because in the real world you don't end up writing that mythical portable C code too often.
I have 'C' code that was written in 1985 that runs today, mostly after moving function parameters from inside the function to inside the argument list.
'C' seems to be a language that makes it easy to write incomprehensible statements, but it's not that hard (or it was not that hard) to write code that is trivial bring up to date. Perhaps it was easier for me because I started with Fortran.
> Maybe it's because the C was four times older, but I think it's because in the real world you don't end up writing that mythical portable C code too often.
It's because C, over time, went from a bunch of almost compatible implementations, to a standard that differed a little from every existing implementation, to a standard that is updated every decade or so, which is then implemented by a bunch of different products, with enough ambiguity in the standard (UB) that there will be differences between compilers and versions of compilers.
Rust is a single implementation. Always has been.
It's a single product, so you code to the product. The product has no external forcing function, like a language standard, that produces changes every decade. When you write Rust, you aren't thinking "Wait, lets make sure this works on the Watcom compiler too".
If Rust code cannot compile 30 years later, that's a failure on the language, because the single implementation is in full control of compatibility. If C code cannot compile 30 years later, that's not a failure of the language, because the implementation in 30 years has had external pressure forcing changes.
People often forget that Rust is a product, C is a specification.
In practice the cadence is exactly half that of WG21's breakneck C++ pace. Every six years. C11, C17, C23 so far and C2y is expected to land before the end of this decade.
But anyway, all I can do is report the observable fact. If you want to say it's because GCC 15 is a different product from GCC 4 whereas rustc 1.99 is the same product as rustc 1.49 then I guess you can re-phrase my position as "In C you will keep needing new products and that sucks" if you like.
In theory, in practice many C developers only know "My C Compiler standard", and are usually surprised that some of their expectations aren't even part of ISO C PDF specification.
Then they get irritated that not all C compiler vendors have the same language extensions or implementation defined behaviours they got used to rely on.
True. In C, it's hard to know what's defined where and what dependencies one is even using unknowingly. The only help is to build on multiple platforms from early on, this will reduce the pain of porting to new platforms later.
It's _very_ easy to write C that is impossible to port to other platforms or other compilers. The reason is that more than for most other ecosystems, there are many different implementations with their own implementations and quirks.
It's also easy to write C that won't build on the system it was originally developed on, a few years down the line. But that is true for almost every other language ecosystem. I would say, C makes it _possible_ to write code in such a way that it works on many different platforms and compilers, and/or can continue to work in decades to come with relatively little maintenance.
The latter isn't true for most other ecosystems, because to get anything done in them, you need to rely more on that language and its ecosystem -- specific language features, specific libraries. C isn't a glue code language. It's very good at letting you create your own thing.
To do it well though, requires a lot of care and expertise.
> more than for most other ecosystems, there are many different implementations with their own implementations and quirks.
Is that the reason? I thought it was primarily because C stupidly* makes a ton of platform-specific things more convenient than the platform-independent equivalent - the width of integer types, locales, etc., all vary by platform, and it's easy to accidentally depend on them.
* Maybe it wasn't stupid at the time when C was developed, but it's a bad choice now.
Integer widths are a problem, but hardly the biggest headache (unless it's a decade old preexisting project that was never designed with portability in mind). Fixed width integer types like int32_t have existed for decades. Even Rust has usize which is machine dependent, and pointers as well have to be machine dependent of course.
Much bigger problem is understanding the scope of dependencies. Dependencies can go bad and you need to update. A dependency might not be available on some new platform so you might have to replace with something else on that platform. To make the software portable, good modularity is required. This is to a large extent an aspect of software architecture. Even Rust can't magically make this happen, if you depend on 300 crates that's probably not a great place to be in either if you want to be portable and maintainable.
There is probably a point that C's flat namespace is a major contributor to badly designed software, because people aren't aware of the dependencies they're mixing all the time. Also C encourages transitive includes, leaking implementation details to the user instead of just to the compiler. (But btw. I find C++ and Rust namespace to be unergonomic syntax-wise, and what's needed is not actually namespaces but control over visibility).
* Programmers are lazy, they're going to type int more often than they should
* All these fancy modern "sized" types are actually just aliases for C's built-in types like int chosen to match the sizes people want...
* ...but C's integer promotion and silent rules about conversions work only on "real" C types you were trying hard not to think about and all the rules for those types, including types you never mentioned but were using because of promotion, apply to your program. You can write code which looks like it's all unsigned, but oops, under the hood a promotion means a signed integer came into existence, then it overflowed, now you have Undefined Behaviour.
I am interested to hear what you don't like about the namespace rules, in Rust in particular.
True that integer promotion rules are defined in terms of the machine dependent unsized "real" integer types. That is indeed annoying. I guess I've found ways to cope. Good thing is, those promotions never cast down, they only widen types.
The point of namespaces is exactly to allow less qualified names, to allow spending less energy when creating new names, by having the compiler always add a qualification implicitly. However, that doesn't solve the problem with name collisions when reading or grepping the source code -- it actually makes it much worse. And avoiding those name collisions, i.e. what's in the source literally, what the programmer reads, and what less-than-perfect tools see, is at least as important as avoiding "semantic" name collisions and being able to compile the code.
Namespaces not only encourage many more literal collisions -- now a single name can be available in various shortenings, and even under different aliases, which also makes it hard to tell that two symbols refer to the same thing!
I've found that the only way to get names right is to find a good scheme, to find meaningful good prefixes, suffixes, and abbreviations, and to come up with a system to create new names more mechanically but still keep them as short and readable as possible.
I think I mostly agree with you about what namespaces should do for us, but somehow I end up liking Rust's choices and you didn't. Interesting.
I like that core::cmp::Ordering and core::sync::atomic::Ordering exist because in their contexts I think these types should both be named Ordering and so it would be annoying if they were named Ordering and AtomicOrdering or something. If I'm writing code which does tricky atomic operations I can bring in core::sync::atomic::Ordering and now Ordering means that one, whereas if I'm writing code about whether this Goose is before or after that Goose, I ask for core::cmp::Ordering to get the relevant Ordering.
And if it's unclear in context, I can write core::sync::atomic::Ordering and everybody knows which one I meant.
I don't usually use an IDE but I believe it's usual in an IDE to tell you the full names of such things and even to help you import relevant stuff based on completions.
I find both of these names too long and the colons are noisy. The fact that I can refer to them using different qualifications makes me uneasy. For both we could find better names, e.g. MemoryOrder is already an established term.
I've realized that naming is an art that is difficult to master but makes a huge difference when gotten right. It consists of not just combining names into compounds but also finding new shorter names that are ergonomic and distinguishable. For example, I've replaced the compound "NodeIntersectTest" in my current project with simply "Probe", which is much easier to read and write, easy to distinguish from other terms after learning its meaning once (it is a central concept to my app), and also perfect for log messages and stats where brevity matters a lot.
A two letter prefix for all identifiers in a project or company codebase is a good idea, but I try to avoid
hierarchies and finer granularity, and generally to be overspecific, in order to keep things easier to change later.
> But, in 2026, I don't see why for native code everyone shouldn't be programming in Rust / Swift / other modern memory-safe flavor for any new production use.
The lack of OpenMP for Rust etc. seems like the most obvious reason for "why use C instead of Rust in 2026" to me.
It's not impressive, it's just a fun thing to show people who learned traditionally and never thought to question the commutativity of the syntax (or whether it is commutative at all)
We had a lot of success with LISTEN/NOTIFY when we paired it with a Rust graphql subscription broker. 10s of thousands of subscriptions, but only 3 or 4 LISTEN connections (one for each host). All changes would be pushed out to all hosts, who would each manage the actual user subscriptions and choose what to actually publish.
This worked super well. In general, moving from hundreds of Ruby or Node hosts to just a few Rust hosts just allows so many simplifications and things that "don't scale" to actually work quite well.
Any graphql library would work. I like async-graphql except compilation is a bear due to all the macros. We wired LISTEN up to subscriptions using tokio mpmc channels. It was a great moment of "fearless concurrency", it all just worked.
I like 'async-graphql'. I tried 'juniper' but, at the time, it didn't have as many features. That may have changed over the last year or so since I looked at it.
Just a set of things too minor to move off of it but annoying enough to not want to start with it.
My list:
No `explain (analyze,buffers)`. Instant DDL has some warts (e.g. fk, metadata locks). Query planning bugs (actually... query planning in general is disappointing). Exiting the repl doesn't stop queries. Implicit type casting. Replication lag from large DDL (e.g. creating an index). Lack of two phase DDL (creating constraints NOT VALID and then VALIDATE later). Lack of extensions (e.g. pg_vector). No safe access to inspect buffer cache. AWS Aurora seems to only add shiny new things to Postgres. And more.
Again, none of this is quite enough to migrate off of it for an established system, but certainly enough to avoid it on a new project.
> This is the number one thing I optimize for now: the ability to quickly and safely change significant parts of the code and product.
This was always a good thing. Its value has nothing to do with the advent of AI coding.
> The opportunity cost of not being able to safely restructure has gone up substantially.
This bit is contradictory with everything else you said. Prior to AI coding it would take a lot longer to perform restructures. If anything, the thing you're now optimising for has gone down in value. It's still valuable, but perhaps a little less.
But in seriousness, this intuitively feels like something (as phrased) that would be easily influenced by loud noise and quantity rather than hard facts. The "piloted poorly" part is applicable to any tool use. AI is no different there other than its adoption rate.
The pattern I observe is: "write code, write test, make things green"
This is different in two ways from the classic TDD red-green-refactor suggestion:
1) they don't start with the test first, so the tests that get implemented are after writing the code, and run the risk of the model attention being now more influenced by the just-written code than the original spec
2) they finish when everything is green and don't followup with the "refactor" step unless manually prompted (either directly or indirectly by your own scaffolding/rules/whatever). this results in frequent hyper-local non-ideal-longterm fixes for things that went wrong in the first shot at writing the code pre-test-writing.
(As always, the only person who can ensure the code landing in your repo is good is you.)
But here's the rub - if you want your clanker to do those steps, it's usually a simple matter of adding them to your AGENTS.md and then it always does them.
I'm a big fan of the characterization step step being added. And it can be reasonable to add this before or even after the fact as a commit prior to your actual commit (assuming you're familiar with using tools where that's easy to do - e.g. jj or git with rebase). And the agents can do this - they just don't tend to without you saying to do so.
A lot of engineering practice comes from choosing which elements are reasonable to use given the context of what you're building. Providing that is your job. When you do that poorly, you get poor results. But garbage in garbage out has always been a thing. Any advanced automation amplifies ambient assumptions
I'm not talking about time. I'm talking about safety. The amount of times I've seen "I refactored it, but I'm not confident enough to take it to prod" is significant. Being able to go faster but still not ship it is the huge opportunity cost.
Time, safety and cost are one and the same. Not safe enough? Spend more time increasing confidence. Taking too long? Cheap out now and pay the price later due to added risk.
All of that is orthogonal to AI. All AI did was accelerate the typing code part - which was never the bottleneck or a very significant cost to begin with.
In the broader corporate world, that's not "opportunity cost". All changes are considered "risk".
All deployments must be approved by an advisory board. All work must originate from a clear business need. Analysis of those needs is not concerned with implementation, least of which whether "AI" is used.
What matters far more is that a contract requires work to be done by a deadline. Those deadlines are driven by policy. There will be no adjustment to policy unless tangible benefits are shown from more frequent deployments of code.
I gotta tell you that's extremely unlikely if you're already shipping every other week at the end of the sprint. Most of that sprint is spent in meetings, not writing code. Nobody is doing big refactors because it wasn't built so fast to require them. There's some technical debt, but nothing so severe. Those meetings are preventing risk, not wasting time. The bottleneck is a feature, not a bug.
If you think the future of dev work is to be a bureaucrat, you're right! It looks like the rest of the world outside of SV is ahead of the curve and living in the future.
I mean "We can't build X because our code structure makes that difficult" has an opportunity cost of the value of X.
I don't think the future of dev work is being a bureaucrat. I've done more rigorous engineering the last two years than I did previously. I'm more confident in the things I shop and they were built in a fraction of the time. It's a bright future for software engineering.
> The cost of shoring up behavior with tests ahead of a restructure has gone down because of AI.
Disagree. The growth in brittle AI-generated tests means restructuring is more costly than it was before. Pruning your test suite so that it tests the essence of the problem and not the incidental design decisions is something AIs aren't yet capable of.
Oh, it's worse than that. I've seen a rise in mutation testing, intended to ensure any change in implementation is caught by tests. Think about that for a moment. It's giving a fancy name to creating brittle tests than fail if any line of code is changed.
And this is seen as a good thing, because LLMs are really bad at confining their changes appropriately. Testing is really in a dark place right now.
> The cost of shoring up behavior with tests ahead of a restructure has gone down because of AI.
Yes.
But the ease of not doing that and instead just getting a brittle set of three-quarters-baked tests is extremely high! And many people seem happy to go from "a few human-written mediocre brittle tests" to "a bunch of AI-written mediocre brittle tests" because it is an objective improvement and the people who weren't avoiding speculative structure and looking for the write boundaries before are happy to also not do so no.
So completely agree with the "take advantage of the tools this way" but I also wouldn't claim it's a reason to no longer worry about if you're building the wrong castles in the sky too early, because perfect refactor-proof testing contracts are still usually pretty hard to design.
> perfect refactor-proof testing contracts are still usually pretty hard to design
Not sure about hard, but definitely rare and we as an industry are under-skilled in these areas.
We have decades of research and tools for testing and verification of software. Property tests, dependent types, formal verification and proof, etc. The paths have been there, we have just collectively prioritized other things.
It requires an intentional shift in how we design and build. That shift is the harder part.
> the ability to quickly and safely change significant parts of the code and product.
Hum, this reminds me something... "O: open to extension, closed to modifications". Old things are new again.
From context efficiency with approaches such ad DDD and clean architectures, all the way to items such as this one, AI is not creating new tradeoff, it just acts as a multiplier, multiplying productivity for teams doing things right, and multiplying debt for teams having a low quality bar as far as design and architecture are concerned.
Your increasing thrash betting that AI will fix it for you. The only thing your getting in return is not having to think that hard.
It doesnt cost that much time or effort to think hard, so you will be outcompeted by people levergaing AI as much as you, but thinking enough to not have it be thrashing around
> The cost of implementing a zero downtime migration has gone down because of AI.
You either don't know what that technical term means or you're just wrong. AI does not meaningfully move the needle on that. It only makes backwards compatible deployments easier insofar you're able to do the overhead for splitting the change with less effort then before.
I've always known how to do zero downtime migrations. The question has always been "Is the engineering cost worth it?"
> It only makes backwards compatible deployments easier insofar you're able to do the overhead for splitting the change with less effort then before.
Yes. That reduces the cost of implementing them.
To be clear, I'm not talking about "Split my db migration and my code that depends on the new table". I'm talking about things like "Set up dual writes between an old database schema and a new database schema with a thorough test suite and do shadow reads against both datasets in prod to do differential testing". That's nontrivial engineering effort that would definitely warrant a discussion in the past. Today we just do it. It's fast and lowers risk.
Generation is basically just memory bandwidth math.
Each token has to read all the active weights. I think that's around 40B parameters active. At a 4-bit quant that's 20GB. With 100GB/s (replace with whatever your bandwidth is) and you get 5 tokens per second.
MTP on a MoE is hit or miss. If you're bottlenecked on memory, MTP can increase the number of active experts (like any batch processing would), which can eat away gains from it.
My favorite lens on SQLite is that it is actually two things:
1. A robust durability implementation
2. A library of high performance data structure and algorithms
The fact this it's SQL is nice, but those two attributes are what make it great.
For example, I'm implement an in-process event log that I want to be durable. I started simple, but soon saw some edge cases and instead of playing whackamole I just swapped to using sqlite as an ordered kv store that gives me ACID.
Another example: ingesting multiple inter related datasets. Instead of a dozen hash maps in memory, I load them up into sqlite (no persistence) and then slice and dice as I need to.
I wonder how Sonnet vs Opus stacks up in a similar time-based comparison.
How far behind are open models compared to Sonnet?
It may be that the absolute SOTA models are way ahead of open models, but the gap in the mid tier really does feel like it's compressing. I'd love to see empirical data about it though.
I'm running opencode with qwen3.6-35b-a3b at a 3-bit quant. I also have qwen3.5-0.8b used for context compaction. I run with 128k context.
It's usable. I set it loose on the postgres codebase, told it to find or build a performance benchmark for the bloom filter index and then identify a performance improvement. It took a long time (overnight), but eventually presented an alternate hashing algorithm with experimental data on false positive rate, insertion speed and lookup speed. There wasn't a clear winner, but it was a reasonable find with rigorous data.
One of my favorite things to show just how bare this is in C is to show array access commutativity.
C is wonderfully simple at times.reply