6 August 2026
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.
macro and raw macro with @name(...) calls; normal macros are hygienic, raw macros splice literally<Section title: 1> ... </Section> in statement position, with attributes read as attributes.name+= -= *= /= %= <<= >>= &= |= ^=, desugared in the parser so they yield a value like =&. |. ^. and unary ~, integer operands of matching width&& / || miscompilation fixed — now a dedicated error pointing at and / or instead of silently compiling to the left operandraw opaque — a real opaque type, castable to and from any *T; *void is rejected in its favourva_list and function-pointer parameters import; unsupported declarations are skipped individuallymut, lend, view, unique, share, belong parse and type; writing through a view reports E4004when / match — arms written (pattern) ~> body, with a mandatory-last (_) defaultfor (init; cond; step), generic parameter lists on every declaration form, and @sizeOf / @offsetOf / @alignOfzithc test, zithc deps list, zithc docs, plus --cache-statsHygiene 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.
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.
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.
29 July 2026
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.
struct Point { x: i32, y: i32 } resolves field names and typesPoint { x: 1, y: 2 } builds a value with named fieldsp.x reads a field from a struct valuep->x reaches through a struct pointer (desugars to deref + dot)&x and *p as prefix operatorsa[i] lowers on arrays, slices, and pointers? with operand and return-type validation
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.
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.
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.
24 June 2026
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.
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.
21 June 2026
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.
/// and /** */ are captured during scanning and rendered above the signatureimport / from / export
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/.
Grab it from the VS Code Marketplace.
Open any .zith file and the LSP starts on its own. There's nothing to
configure.
20 June 2026
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.
memory::Spanstd::vector → DynArray — ZIR structs and Diagnostics now use arena-backed storagestd::array instead of a heap vector, for predictable costDynArray, 19 for FlatMap
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.
20 June 2026
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.
Optional, Result, Arena, DynArray and FlatMap now live in their own repo<unordered_map> and <unordered_set> are gone from the compiler source
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.
19 June 2026
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.
walkSubTypes() visits sub-types recursively, replacing manual type-kind switchesplatform.hpp centralizes OS and compiler detection macrosoccurs() check went from a 30-line switch to a five-line walkSubTypes() call
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.
13 June 2026
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.
Resolve stage between import and sema, handling qualified and unqualified namesimport foo as f resolves through the symbol table with target trackingNotNamespace) and missing members (NoMember).., arrow access ->, fat arrow =>, plus a RangeNode in the AST_WIN32 guards across five CLI command files
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.