A research-stage frontend compiler that lowers .zpp source — Zig with extra
language affordances — into plain .zig and hands it off to the upstream Zig
toolchain.
Status: proof of concept. The full design is laid out in
Zig++ Design Draft v0.1; this repository contains the minimum-but-actually- running implementation, not the finished language.
Zig++ is the experiment of asking: "what would C++ have looked like if it had been built on Zig instead of C?" The constraint is to add high-level abstractions without breaking Zig's core principle that costs are visible:
- No hidden control flow.
- No hidden allocations.
- No hidden destructors.
- No macros.
The added surface so far:
- Named
traitinterfaces. impl Trait(static dispatch via comptime generics) anddyn Trait(explicit fat-pointer dispatch).- Explicit RAII via the
usingkeyword. - Affine ownership markers:
own var,move. effects(.noalloc, .io, ...)annotations on functions.- A small set of compile-time ownership checks.
Everything goes through a text/lexer-based lowering pipeline that produces human-readable Zig — you can read the output and understand what happened.
.zpp source
|
v
+--+----------------------------------+
| Zig++ frontend |
| line rules (using/own/move/...) |
| structural trait lowering |
| ownership checks (E0001/2/10) |
+--+----------------------------------+
|
v generated .zig
|
v
+--+--------------+
| zig (upstream) |
+--+--------------+
|
v binary / library
The compiler/ directory implements the frontend; everything else (codegen,
linking, optimization) is delegated to Zig itself.
- Zig 0.15.2. This is what was installed during development; later versions
may break the build.zig. The project's
build.zig.zonadvertisesminimum_zig_version = "0.15.0".
# Build the zpp binary and the zpp_lib static library.
zig build
# Run all unit tests (lexer, lowering, ownership checks, etc.).
zig build test
# Run the .zpp integration test suite.
zig build test-zpp
# Lower a single .zpp file and print the generated .zig to stdout.
./zig-out/bin/zpp lower path/to/file.zpp
# Walk a project directory, lower every .zpp into <dir>/.zpp-out/.
./zig-out/bin/zpp build path/to/project
# Lower a project, then build and run it via `zig build run`.
./zig-out/bin/zpp run examples/hello_runnable
# -> hello, zigpp!
# Run all ownership / effect checks on a project (no codegen).
./zig-out/bin/zpp check examples
# -> [zpp] checked 5 files, 0 findingsusing file = try File.open("log.txt");
try file.writeAll("hello\n");Lowers to:
var file = try File.open("log.txt"); defer file.deinit();
try file.writeAll("hello\n");Spelled-out at the call site. No invisible destructor at scope exit.
trait Writer {
fn write(self, bytes: []const u8) !usize;
fn flush(self) !void;
}Lowers to a real Zig fat-pointer struct with a vtable and dispatch wrappers:
pub const Writer = struct {
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
write: *const fn (self: *anyopaque, bytes: []const u8) anyerror!usize,
flush: *const fn (self: *anyopaque) anyerror!void,
};
pub fn write(self: Writer, bytes: []const u8) anyerror!usize {
return self.vtable.write(self.ptr, bytes);
}
pub fn flush(self: Writer) anyerror!void {
return self.vtable.flush(self.ptr);
}
};fn emit(w: impl Writer, msg: []const u8) !void { _ = try w.write(msg); }
fn logAll(w: dyn Writer, msgs: []const []const u8) !void { ... }impl Writer lowers to anytype — comptime-monomorphized static dispatch.
dyn Writer lowers to Writer — the fat-pointer struct above.
own var buf = try Buffer.init(allocator, 4096);
var moved = move buf; // buf is now dead
consume(move moved);Both keywords are stripped at codegen — they're affine-ownership markers for the static checker, not runtime constructs.
effects(.noalloc, .noio)
pub fn hashBytes(bytes: []const u8) u64 { ... }Lines consisting solely of effects(...) are stripped at codegen and recorded
for the analyzer. Functions tagged .noalloc get a hidden-allocation check.
Five token-based ownership/effect checks ship with this proof of concept:
| Code | Meaning | Detection |
|---|---|---|
| E0001 | Owned value was not deinitialized | own var x = ... with no matching using x / x.deinit() / move x / alloc.destroy(x) |
| E0002 | Owned value used after move | Use of an identifier later than its move site, in the same scope |
| E0003 | Owned value deinitialized twice | Two or more <name>.deinit() calls per own var <name>, same scope |
| E0004 | Allocator mismatch | own var x = a.create(T); b.destroy(x); — release allocator differs from create allocator |
| E0010 | Hidden allocation in noalloc function |
Allocator method calls or pass-through inside effects(.noalloc) body |
zpp check <dir> runs all five and prints <path>:<line>:<col>: <code> <message>.
The same checks back the LSP server (zpp lsp), so editors with the bundled
VS Code extension see findings in real time.
These are token-stream checks, not a full semantic pass — sufficient for the fixture suite, intentionally limited otherwise. E0020 (trait-not-implemented) is declared in the diagnostic namespace but not yet implemented; it would benefit from the AST refactor.
zigpp/
build.zig top-level build script
build.zig.zon package manifest
compiler/ frontend
lexer.zig Zig++ tokenizer (Zig + Zig++-specific keywords)
lower_to_zig.zig line-level lowering rules + pipeline entry
trait_lower.zig structural trait -> vtable lowering
checks.zig ownership / noalloc analyzers
project.zig recursive .zpp -> .zig project walker
diagnostics.zig diagnostic types and code constants
parser.zig, sema.zig, ast.zig stubs for future AST-based pipeline
root.zig zpp_lib import surface
lib/ runtime support library (zpp.* namespace)
owned.zig Owned(T), Borrow(T), ArenaScope, DeinitGuard
contracts.zig requires / ensures / invariant
dyn.zig, traits.zig, async.zig, testing.zig (skeletons)
tools/ CLI binaries
zpp.zig main `zpp` driver
zpp_fmt.zig, zpp_lsp.zig, zpp_doc.zig, zpp_migrate.zig (skeletons)
examples/ .zpp demos
hello_runnable/ end-to-end runnable Zig++ program
hello_trait.zpp, owned_file.zpp, dyn_plugin.zpp, async_group.zpp
tests/ integration runner + fixtures
test_runner.zig walks fixtures, dispatches per category
compile/ must-lower-cleanly fixtures
behavior/ programs whose stdout/stderr is asserted
lowering/ byte-exact .expected.zig golden tests
diagnostics/ must-emit-Exxxx fixtures
no_hidden_alloc/ positive .noalloc conformance
zig build OK
zig build test OK (~140 inline tests across the compiler / lib / LSP modules)
zig build test-zpp passed: 21 failed: 0 skipped: 0
CI (Ubuntu + macOS, Zig 0.15.2) runs zig build, zig build test,
zig build test-zpp, plus four end-to-end smoke tests on every push:
zpp run examples/hello_runnable, zpp check examples, zpp fmt --check .,
and the LSP initialize handshake.
Categories exercised:
compile/— fixtures must lower without error.behavior/— fixture is lowered, compiled, run; stdout/stderr is asserted.lowering/— fixture is lowered and compared byte-exact to a.expected.zig.diagnostics/— fixture must produce a specific Exxxx finding fromchecks.zig.no_hidden_alloc/— fixture must produce zero E0010 findings (positive case).
- No real parser yet. The lowering and the checks operate on the token stream
with brace counting and a single-line lexical state. Documented limitations
apply (multi-line strings, multi-line
effects(...)calls, scope rebinding formove). zpp fmt,zpp doc,zpp lsp,zpp migrateare CLI placeholders that panic on use.derive(.{Hash, Json, ...})is in the design but not lowered.extern interfaceblocks pass through verbatim — only baretraitis lowered.- The
lib/runtime helpers are real but minimal; many functions are skeletons.
The full language vision lives in the original Zig++ Design Draft v0.1. This
repo implements only the most load-bearing slice of it — enough to demonstrate
that the pipeline works end-to-end and that the headline features (using,
trait, impl, dyn, ownership/effect checks) can be lowered to plain Zig
without invisible cost.
Two minimal editor integrations live in editors/, both backed
by the same zpp lsp server (see tools/zpp_lsp.zig):
editors/vscode/— VS Code extension. Install: see editors/vscode/README.md.editors/neovim/— Neovim plugin (Lua, Neovim 0.10+). Install: see editors/neovim/README.md.
See CHANGELOG.md for the development log.
MIT. See LICENSE.