Karya Semi
HomeBlogSearchCategoriesAboutContact
Karya Semi

Less noise. More notes.

HomeBlogAboutContactPrivacy PolicyDisclaimer

© 2026 Karya Semi. All rights reserved.

XGitHubLinkedIn
  1. Home
  2. /Categories
  3. /Programming

C Finally Got Tail-Call Optimization. 50 Years Late.

C got tail-call optimization in 2025, decades after functional languages had it. The technical and political reasons behind the delay.

Dian Rijal Asyrof/August 11, 2026/6 min read
Illustration for C Finally Got Tail-Call Optimization. 50 Years Late.

In 2025, the C standards committee finally added tail-call optimization to the language. Not as a compiler extension. Not as a vague suggestion buried in an appendix. As an actual language feature, standardized across compilers.

If you write Haskell, Scheme, or Erlang, you're probably confused. Those languages have had proper tail-call optimization since the 1970s and 1980s. Scheme's specification literally requires it. So why did C, arguably the most influential programming language in history, take over 50 years?

The answer is messier than you'd expect. It involves compiler engineers arguing at conferences, backwards compatibility nightmares, and a genuine philosophical disagreement about what C is supposed to be.

What Tail-Call Optimization Actually Means

A tail call happens when a function's last action is calling another function (or itself). Here's a simple example:

int factorial_tail(int n, int acc) {
    if (n <= 1) return acc;
    return factorial_tail(n - 1, n * acc);  // tail call
}

That return factorial_tail(...) is the last thing the function does. There's no multiplication waiting to happen after the call returns. The function just passes the result straight through.

In theory, the compiler can notice this and replace the recursive call with a jump. Instead of creating a new stack frame each time, it reuses the current one. The recursion becomes a loop under the hood. No stack overflow. No wasted memory. Same performance as a while loop.

Without tail-call optimization, that factorial function blows up your stack if you pass in a large number. With it, factorial_tail(1000000, 1) runs fine.

Simple idea. Obvious win. So what went wrong?

Functional Languages Got There First for a Reason

Languages like Scheme and Haskell were designed around recursion. In pure functional programming, you don't have mutable loop variables. You recurse. If recursion is your primary control flow mechanism, tail-call optimization isn't a nice-to-have, it's the difference between your programs working and your programs crashing.

Scheme made tail-call optimization mandatory in its original 1975 specification. Not optional. Not "quality of implementation." The language does not work without it.

C has a different story. C has for loops. C has while loops. C has goto. Recursion in C is something you reach for when a loop doesn't fit the problem cleanly, not your default way of repeating things. So from the committee's perspective, optimizing tail calls was a quality-of-life improvement, not a correctness requirement.

That framing mattered more than anyone expected.

The Technical Barriers Were Real

Here's something people forget when they complain about the committee being slow: C's execution model fights tail-call optimization at every level.

Stack frame layout. C functions often have local variables whose addresses are taken and passed around. That pointer needs to point somewhere valid for the function's entire lifetime. If you reuse the stack frame for a tail call, the old function's locals vanish, but something might still hold a pointer to them. This is undefined behavior territory, and C compilers tend to be conservative about UB. Modern systems languages like Rust solve similar pointer lifetime issues with concepts like the borrow checker, which can be explored in our guide to Rust ownership and the borrow checker.

Debugging. If the compiler reuses stack frames, your debugger shows a flat call stack even when the actual call chain is deep. GDB, LLDB, and every other C debugger assumes stack frames map 1:1 to function calls. Breakpoints in tail-called functions get weird. Stack traces lie to you. Profilers get confused. Compiler engineers spent years trying to make tail-call optimization play nicely with -g debug info, and the results were never great.

ABI compatibility. On x86-64, the System V ABI specifies exactly how functions set up and tear down their stack frames. Tail-call optimization sometimes requires different register allocation or stack cleanup sequences. If your optimized code calls a shared library that wasn't optimized the same way, things break in subtle, architecture-specific ways. The compiler can optimize internal calls safely, but it can't always guarantee correctness across translation unit boundaries.

alloca and VLAs. C lets you allocate memory on the stack dynamically. This completely breaks tail-call reuse because the stack pointer isn't where the callee expects it. Any function containing alloca or variable-length arrays is ineligible, and checking for these adds complexity to the optimizer.

Each of these problems is solvable. But stacked together, they represented years of engineering work that nobody was willing to fund.

The Committee Politics

The C standards committee (ISO/IEC JTC1/SC22/WG14) operates by consensus. That means proposals need broad support, and broad support means addressing every concern from every major compiler vendor.

GCC, Clang, Microsoft's MSVC, and the embedded compilers (IAR, Green Hills, various vendor-specific ones) all have different optimization pipelines. A tail-call optimization standard needs to be specific enough to be useful but flexible enough that every compiler can implement it without rewriting their backend.

For years, proposals bounced between "must perform TCO" (like Scheme) and "may perform TCO" (which compilers could already do as an optimization). The first camp argued that without a guarantee, you can't write portable recursive code that won't blow the stack. The second camp argued that guarantees create portability traps since not every compiler can deliver on every platform.

There's also a cultural divide. Some committee members come from the embedded systems world, where stack usage is audited line-by-line and any invisible optimization is suspect. Others come from compiler research, where TCO is a solved problem that's been working in LLVM for over a decade. These groups don't always speak the same language.

And honestly, C had other fires to put out. C11 added atomics and threads. C23 cleaned up decades of accumulated weirdness. Tail-call optimization kept getting bumped because it was seen as a quality-of-life thing, not a correctness thing.

What Finally Changed

A few things converged in the early 2020s.

First, security researchers started publishing real-world exploits where deep recursion in C code caused stack overflows that opened vulnerabilities. Buffer overflows get all the attention, but stack exhaustion bugs were becoming a serious attack surface, especially in parsers and protocol handlers. Making tail recursion safe stopped being academic.

Second, the WebAssembly crowd applied pressure. Wasm's execution model is naturally friendly to tail calls, and Wasm-targeting C compilers wanted a standard way to emit them. If you're compiling C to Wasm, you need TCO to avoid blowing up the tiny default stack. Setting up reproducible toolchains for such specialized targets is a challenge, which is why solutions like reproducible AI workstations using NixOS are gaining traction.

Third, LLVM and GCC had independently matured their TCO passes to handle most of the edge cases. The engineering work was largely done. What was missing was the standardization glue that lets programmers write portable code.

The C2y working draft (what people are calling C26, though the numbering isn't final) includes [[musttail]] as an attribute. The syntax signals the compiler: "I've verified this call site is safe for tail-call reuse. If you can't optimize it, error out." This is the right approach. It puts the burden of verification on the programmer (who knows the code's constraints) and gives the compiler a hard guarantee it can trust.

int process(int state, int input) {
    // ... computation ...
    [[musttail]] return next_state(new_state, new_input);
}

If the compiler can't do the optimization, you get a compile-time error, not silent stack growth. That's a big deal. It means you can rely on the optimization instead of hoping.

Why "Musttail" Instead of Implicit Optimization

Some people wanted the compiler to figure it out automatically, like GCC's -foptimize-sibling-calls flag has tried to do for years. But implicit optimization creates a trap. You write code that works great on GCC with -O2, ship it, and then someone compiles it with a different compiler or a different optimization level and the stack overflows in production. Silent optimization that sometimes works is worse than no optimization at all.

[[musttail]] makes the contract explicit. The programmer says "this is a tail call," the compiler says "confirmed" or "nope, can't do it here." No ambiguity. No platform-specific surprises.

It also sidesteps the ABI problem. If a call site has [[musttail]], the compiler knows it can safely reuse the frame. If it doesn't have the attribute, the compiler behaves exactly as it always has. No existing code breaks.

What This Means for C Developers

If you write recursive C code today, you're probably already managing stack depth manually or using trampolines. The new attribute doesn't change your existing workflow overnight.

But it opens up patterns that were previously impractical. State machines expressed as mutually recursive functions. Parser combinators that don't blow the stack on deep input. Recursive descent algorithms that were rejected in favor of iterative versions purely for performance reasons.

And for the people compiling C to WebAssembly or running on embedded devices with 4KB stacks, [[musttail]] is a genuine quality-of-life upgrade they've been waiting for.

Took 50 years. But C got there eventually. The language that values explicit control over implicit magic found an explicit way to express it. That's about as C as it gets.

DR

Dian Rijal Asyrof

Writes about useful AI tools, programming practice, and the craft of building reliable software.

Previous articleMeta Ships a 30B Model That Lives on Your Laptop, Not Their CloudNext articleNobody Budgets for the Real Cost of an AI Agent
C LanguageCompilersTail Call OptimizationProgramming HistoryPerformance
On this page↓
  1. What Tail-Call Optimization Actually Means
  2. Functional Languages Got There First for a Reason
  3. The Technical Barriers Were Real
  4. The Committee Politics
  5. What Finally Changed
  6. Why "Musttail" Instead of Implicit Optimization
  7. What This Means for C Developers
  8. Took 50 years. But C got there eventually. The language that values explicit control over implicit magic found an explicit way to express it. That's about as C as it gets.

On this page

  1. What Tail-Call Optimization Actually Means
  2. Functional Languages Got There First for a Reason
  3. The Technical Barriers Were Real
  4. The Committee Politics
  5. What Finally Changed
  6. Why "Musttail" Instead of Implicit Optimization
  7. What This Means for C Developers
  8. Took 50 years. But C got there eventually. The language that values explicit control over implicit magic found an explicit way to express it. That's about as C as it gets.

See also

Illustration for Branchless Rust: Accelerating Data Filters by Eliminating Conditionals
Programming/Aug 6, 2026

Branchless Rust: Accelerating Data Filters by Eliminating Conditionals

Branch prediction failures can slow down tight hot loops. A practical look at implementing branchless programming patterns in Rust to speed up filter functions.

4 min read
RustOptimization
Illustration for Your SIMD Code Doesn't Need the CPU Anymore
Programming/Aug 11, 2026

Your SIMD Code Doesn't Need the CPU Anymore

VectorWare's breakthrough enables direct GPU execution for Rust SIMD, revolutionizing rust simd gpu programming 2026 for developers seeking peak performance.

4 min read
RustSimd
Illustration for Building textlog, a Quiet and JavaScript-Free Microblogging Platform
Web Development/Aug 8, 2026

Building textlog, a Quiet and JavaScript-Free Microblogging Platform

Analyze the textlog quiet text only microblogging platform architecture no js codebase, engineered for minimal server footprints and offline-first syncing.

8 min read
Web ArchitectureHtml Only