v0.6.0 — Macros, Operators & the Ownership Boundary

6 August 2026


What

Two big pieces landed together: metaprogramming and the operator surface. Macros now expand through the entire pipeline, the operators that were missing are here, and ownership finally has somewhere to run. Semantic facts get produced and consumed before final lowering, on the stable sema -> comptime/solve -> NTA/NRA -> HIR boundary.


Highlights


Under the hood

Hygiene works because macro expansion resolves names by node id instead of matching source spans. Template-local bindings get renamed, every other name is looked up in the call-site scope, and the template body never goes through analysis as ordinary code.

Compound assignment, on the other hand, adds no HIR node at all: the parser rewrites a += b into Assign(Binary(a, b)), so coercion and the view write check come along for free.

The ownership work is structural, so you won't see much of it from the outside. Residual facts attach to side tables rather than adding ownership nodes to HIR, which gives the proof a defined place to run before lowering throws away the information it depends on.

Diagnostics are no longer four codes in a trench coat. They're grouped by stage now: lexical 0001-0005, parse 1001-1008 (including the W1008 while deprecation), semantic 2001-2010, macro 2011-2020, type 3001-3008, NRA/ownership 4001-4004, lowering 5001-5002, and runtime 10001-10004.


Still not there

Generic instantiation still breaks in the comptime solver: identity<i32>(42) reports "generic parameter T has no concrete type", so generic code doesn't compile end to end yet. Comptime evaluation and const fn are still specification-only, const fn f() parses as a const binding named fn, dyn Trait isn't supported in the type parser, the NRA alive/dead/lent state machine is unwritten, and for (x in xs) is a parse error. Full detail lives in Implementation Status.


Verification

Every status in the implementation table was re-checked the boring way: one standalone file per feature, run through zithc check against the binary built at a5f3716, rather than against what the spec claims. when is covered by a runtime codegen test, and import "stdio.h" followed by printf("v=%d\n", 42) prints exactly v=42.


v0.5.0 — Structs, Pointers & the Modern Pipeline

29 July 2026


What

Structs are usable end to end. Field declarations, struct literals, field access, and pointer operators all landed in the modern frontend, sema, and HIR lowering pipeline, which means you can build a struct value, read it, take its address, and mutate it all the way down to LLVM code generation.


Highlights


Under the hood

Three new expression kinds (Field, Arrow, StructLiteral) travel through the whole stack. The type table carries parallel field names with a fieldIndex lookup, sema infers the new nodes, and HIR lowering resolves each field to an index before emitting. C interop rode along in the same release, driven by libclang.

The old E2010 UnsupportedSyntax barrier is also gone from the active pipeline; the legacy sema now lives under lib/legacy-zith. What's left of the diagnostic surface is E0001, E0000, E2002, and E1006.


Still not there

when pattern matching still dies as a parse error past the header, const fn parses as a const binding named fn, dyn Trait isn't supported in the type parser, and @ isn't handled in expression position. NRA ownership analysis, comptime evaluation, and the async model are all still specification-only. Full detail lives in Implementation Status.


Verification

zithc check passes on the hello-world example and the suite sits at 23/23. As always, every status in the implementation table was re-checked against the current binary, not against what the spec intends.


v0.4.0 — Introducing zasm

24 June 2026


What

zasm is a standalone assembler for ZIR (Zith Intermediate Representation). It compiles .zas files into ZIR bytecode and runs them on a built-in interpreter. Since ZIR is the same IR zithc targets, zasm gives you a direct path from assembly straight to execution.


Highlights


Still not there

The syntax needs a cleanup pass, since common patterns are far too verbose. Error messages around label and function namespace rules could say a lot more. And the big one: lowering ZIR to LLVM IR.


v0.2.0 — The LSP Awakening

21 June 2026


What

The Zith Language Server is alive, and VS Code finally behaves like a real editor for Zith. Hover a symbol to see its type. Press Ctrl+Space for completions, import paths included. Press Ctrl+Shift+O for a symbol tree that actually nests. Typo something and you get told what you probably meant.

A new VS Code extension packages all of it. The LSP downloads itself on first activation, the standard library is fetched automatically with no credentials involved, and highlighting, snippets, task runners and problem matchers are there from the start.


Highlights


Under the hood

Most of the intelligence lives in the compiler, not the server. SymbolData gained a doc_span field so doc comments survive the scan phase, and zithc_diag_suggestion_count() / zithc_diag_suggestion_get() expose the Levenshtein engine over the C API, which lets the LSP embed suggestions in diagnostic JSON without re-running the compiler.

Two bugs were making the editor look broken for reasons that had nothing to do with the features themselves. respond() dropped the result field for null results, so hovering an unknown token sent a malformed JSON-RPC envelope. And spanToRange carried a span.end <= span.start guard that zeroed every range — the real cause was ScanEntry.span mixing byte offsets with token indices, so every document symbol pointed at line 1.

Document symbol children are now built by matching decl_id against the symbol table and walking SymbolData.members, replacing a hardcoded empty array. Hover no longer aborts when runTo(TypeChecked) fails; type details render conditionally on top of symbol info that is always available. Import errors point at the import keyword instead of line 1, and the zith.lsp.stdlibPath setting is gone — the path is detected from server/stdlib/.


Get the extension

Grab it from the VS Code Marketplace. Open any .zith file and the LSP starts on its own. There's nothing to configure.


Release v0.1.3

20 June 2026


What

The ZIR bytecode IR arrives: 26 opcodes, an emitter from HIR, and a stack-based interpreter ported over from the prototype branch. Alongside it, two more subsystems moved off std::vector onto the arena-backed DynArray, and every AST node now carries a Span.

None of it is wired into the pipeline yet. This release is about having the pieces exist and be testable.


Highlights


Under the hood

ZirBlock::code, ZirFn::blocks and ZirModule's functions and constants all moved to DynArray, which means the emitter takes an Arena& in its constructor and ZIR allocation is bump-allocated along with everything else. Diagnostic followed the same path for its labels and suggestions.

Spans on AST nodes are what makes the LSP possible later, so the parser gained a spanFrom() helper and every parse function computes one. AstBuilder methods take an optional span throughout.

The only real annoyance was narrowing: DynArray::emplace() and push() used brace initialization, which turns a uint32_t into a hard error rather than a conversion. Switching to parens fixed it.


Release v0.3.2

20 June 2026


What

An infrastructure and code-quality release. The core utilities moved out into a standalone zith-util repo, every std::unordered_map was replaced by a custom open-addressing FlatMap, and Span fields went onto every AST node to prepare for the LSP. There is no STL hash map left in the compiler.


Highlights


Under the hood

StringInterner, SourceMap's path cache, ImportManager's index and resolving sets, and TypeLower's generic context all switched to FlatMap. Transparent std::string_view lookup is what made that painless — no temporary strings on the hot path.

One fix worth remembering: FlatMap<string, bool> hits the std::vector<bool> bit-vector specialization, whose proxy reference is not a real bool&. resolving_ now stores char instead, which is the boring correct answer.


Release v0.3.1

19 June 2026


What

A stability release. It adds a Type Walker for traversing the type graph without hand-written switches, and a platform abstraction layer that cuts the #ifdef noise, then clears out seven bugs — several of them the kind that do not show up until they do.


Highlights


Under the hood

The fixes are the substance here. TypeFn held a span that dangled after the arena reallocated, iterating an empty token stream ran off the end, and the lexer had a path that fell through a nullptr check. SourceMap had a TOCTOU race between the file-size check and the mmap, which is now a single atomic step.

Three smaller ones came along: module path depth is properly bounded during recursive descent, DynArray::back()'s const overload returns const T& as it always should have, and Optional<T*> holding a nullptr no longer trips an assertion.

TypeWalker itself is a generic walkSubTypes(TypeData&, Fn&&) that dispatches on TypeKind, visits every child TypeId and supports early exit through a bool return. Unification is the first consumer; it will not be the last.


Release v0.3.0

13 June 2026


What

Name resolution becomes a pipeline stage of its own, sitting between import resolution and semantic analysis. Once all imports are known, the new Resolver walks the AST, resolves every identifier to its SymId, and writes the result into a side table that Sema reads directly.

The parser also picked up range syntax, arrow member access and fat arrows, and the compiler builds on Windows now.


Highlights


Under the hood

The side table is the design decision worth noting. visitIdent checks the resolver's table first and only falls back to syms().lookup() when there is nothing there — and it skips emitting a diagnostic the resolver already reported, which is how you avoid two errors for one mistake. Resolver::takeResolvedTable() moves the table out for later stages rather than copying it.

Aliases work because SymbolData now carries a target field, so an alias declaration stores what it points at and chains can be followed. ImportManager tracks SymOrigin for every merged symbol, recording which file and local symbol it came from.

Three fixes rode along: the diagnostic emitter crashed on an Optional holding nullptr and lacked a bounds check on d.primary.end, and the import merge path was silently overwriting duplicate re-exports instead of reporting DuplicateDecl. visitIf also stopped discarding the then-branch value and now builds a proper HirBranch.