· 2 min read ·

Zig's Type Resolution Gets a Ground-Up Rethink

Source: lobsters

Zig’s compiler has always had an unusual relationship with types. Because comptime evaluation is woven into the language at a fundamental level, the compiler can’t cleanly separate “figure out what type this is” from “evaluate this code.” Types are values, functions can return types, and any expression might resolve to a type depending on what’s passed in. That makes type resolution genuinely hard, and it shows in the compiler’s history.

The Zig team recently posted a devlog entry describing a redesign of the type resolution system, along with a set of language-level changes that came out of it. This is the kind of work that rarely gets a clean announcement because it’s foundational: it doesn’t ship a feature, it reshapes the ground everything else stands on.

What makes Zig’s type resolution complex

Zig uses what it calls peer type resolution to figure out a common type when multiple branches or values are in play. If you write an if expression where one branch produces a u8 and another produces an i32, the compiler needs to decide what type the whole expression has. That sounds simple until you add comptime, result location semantics, and anonymous struct and enum literals to the mix.

Result location semantics, introduced a few years ago, let the expected type flow inward from context. A function call can receive type information from where its result will be stored, which lets you write things like:

const point: Point = .{ .x = 1, .y = 2 };

without spelling out Point on the right side. The compiler propagates the expected type down into the literal. This is convenient, but it means type resolution isn’t a bottom-up pass anymore. The direction of information flow is bidirectional, which complicates the implementation considerably.

Why redesign it now

The incremental compilation work that has occupied a large chunk of Zig’s recent development put pressure on the existing design. Incremental compilation requires the compiler to re-resolve only what changed, which means the resolution logic needs clear boundaries between what depends on what. A design that grew organically around special cases doesn’t lend itself to that kind of precision.

The redesign appears to have drawn cleaner lines around how types flow through expressions, which is the kind of internal restructuring that makes future work easier even if it doesn’t directly ship new features. The “language changes to taste” subtitle signals that the team took the opportunity, while reworking the internals, to tighten up some edge cases in the language itself.

Language-visible effects

When a type resolution system gets redesigned, some previously-accepted code stops compiling, some previously-ambiguous code gets a definitive answer, and occasionally you get new capabilities that weren’t practical before. The Zig team has generally been willing to make breaking changes when they improve the language’s coherence, and 0.x version numbers exist for exactly this reason.

What’s worth watching is whether the changes make comptime-heavy code easier to write and reason about. Generic programming in Zig is powerful but can be opaque: error messages from deep in a comptime evaluation are notoriously hard to interpret. A cleaner type resolution model should, in principle, produce better diagnostics.

Zig is still finding its final shape. Watching how the team handles this kind of foundational work, willing to redo it rather than paper over the problems, is part of what makes following the project interesting.

Was this interesting?