
Checker for Lifetimes and other Refinement types
Checker of
Lifetimes and other
Refinement types
for Zig
Video: https://www.youtube.com/watch?v=mf0WzTOe-40 Sponsorship: https://buymeacoffee.com/dnautics
discuss on hn: https://news.ycombinator.com/item?id=42923829
discuss on lobste.rs: https://lobste.rs/s/9sitsj/clr_checker_for_lifetimes_other
live demo video: https://www.youtube.com/watch?v=ZY_Z-aGbYm8
This project creates a Zig transpiler for the Zig compiler, which transforms AIR (Abstract Intermediate Representation) into Zig source code that performs static analysis at compile time. The generated analyzer catches memory safety issues like use-before-assignment, use-after-free, stack pointer escapes, as well as Zig-specific UB such as non-nullness assertions, tagged union violations, or fieldParentPtr misuse.
The goal is to bring Rust-level memory safety guarantees to Zig through static analysis of AIR, without changing the language itself.
CLR depends on a forked version of the Zig compiler (included as a submodule in zig/) that adds support for routing AIR to external plugins. When invoked with -ofmt=air -fair-out=<plugin.so>, the compiler loads the specified shared library and passes generated AIR to it for processing.
CLR is intended to push programs toward lifecycle patterns that are explicit and locally verifiable, not merely to recognize every technically valid Zig program. When two representations are possible, CLR prefers the one that makes resource state visible in the type and control-flow structure.
For example, avoid conditionally closing a non-optional file descriptor:
const file = try std.fs.cwd().openFile(path, .{});
if (should_close) {
file.close(); // Bad: file is ambiguously open after this branch.
}
Prefer representing conditional ownership with an optional:
var file: ?std.fs.File = null;
if (should_open) {
file = try std.fs.cwd().openFile(path, .{});
}
if (file) |open_file| {
open_file.close();
}
Conditionally closing a non-optional descriptor leaves its lifecycle ambiguous after the branch. CLR's intended policy is to reject that pattern rather than carry a permanent "maybe closed" state.
The same principle applies to allocated pointers. Do not free through a derived pointer:
const allocation = try allocator.alloc(u8, size);
const payload = allocation[header_size..];
allocator.free(payload); // Bad: payload is not the allocation base.
Keep the allocation-base pointer available for deallocation, and use derived pointers only for access:
const allocation = try allocator.alloc(u8, size);
defer allocator.free(allocation);
const payload = allocation[header_size..];
use(payload);
Freeing a field pointer, subslice, or pointer produced by arithmetic is rejected unless a documented internal rule reestablishes allocation-base provenance.
These policies are strict by default because they produce code with simpler, more reviewable resource lifecycles. A future unsafe annotation mechanism will allow selected GIDs or operations to opt out of individual analyses. That will support code which deliberately accepts weaker checking in exchange for performance, without weakening the default model for the rest of the program.
This is an active rewrite of the original Elixir-based proof-of-concept in Zig. The Zig implementation loads as a compiler plugin and analyzes AIR directly.
Currently implemented:
std.mem.Allocator interface coverage:
create/destroy - single item allocationalloc/free - slice allocation (including alignedAlloc, allocSentinel, etc.)realloc/remap - slice reallocation with old-slice-freed trackingdupe/dupeZ - slice duplicationinit// - full arena lifecyclePlanned (see LIMITATIONS.md for details):
sudo apt install batsThe vendored Zig compiler and libclr plugin must be built with matching optimization levels. Mismatched optimization levels will cause segfaults.
# Build the custom Zig compiler with ReleaseFast (first time only, or after submodule changes)
cd zig && zig build --zig-lib-dir lib -Doptimize=ReleaseFast && cd ..
# Build the CLR plugin with matching optimization
zig build -Doptimize=ReleaseFast
For development/debugging, use ReleaseSafe or Debug for both:
# ReleaseSafe (with safety checks, slightly slower)
cd zig && zig build --zig-lib-dir lib -Doptimize=ReleaseSafe && cd ..
zig build -Doptimize=ReleaseSafe
# Debug (full debug info, slowest)
cd zig && zig build --zig-lib-dir lib && cd ..
zig build
# Compile a Zig file using the AIR backend
zig/zig-out/bin/zig build-exe -fair-out=zig-out/lib/libclr.so -ofmt=air -femit-bin=output.air.zig your_file.zig
# Run the generated analyzer
zig run --dep clr -Mroot=output.air.zig -Mclr=lib/lib.zig
Output goes to stderr.
# Unit tests (codegen/DLL)
zig build test
# Unit tests (runtime library)
zig test lib/lib.zig
# A focused integration test file
bats test/integration/fd.bats
# Integration tests (requires BATS)
# Defaults to ReleaseFast; override with OPTIMIZE=ReleaseSafe or OPTIMIZE=Debug
./run_integration.sh
# Manual test of a single file
./run_one.sh test/cases/undefined/use_before_assign.zig
Note: Integration tests rebuild libclr with the specified optimization level (default: ReleaseFast). Make sure your vendored Zig compiler was built with a matching optimization level.
clr/
├── src/ # DLL/plugin code (generates .air.zig)
│ ├── clr.zig # Main CLR plugin entry point
│ ├── codegen.zig # Generates .air.zig source from AIR instructions
│ └── allocator.zig # DLL-safe allocator wrapper
├── lib/ # Runtime analysis library
│ ├── lib.zig # Library entry point
│ ├── tag.zig # AnyTag union, Type, tag handlers, splat dispatch
│ ├── Inst.zig # Instruction results and interprocedural analysis
│ ├── Refinements.zig # Refinement types (pointer, struct, optional, etc.)
│ ├── Analyte.zig # Analysis state container
│ ├── Context.zig # Execution context (metadata, error reporting)
│ └── analysis/ # Analysis modules
│ ├── undefined_safety.zig # Use-before-assign tracking
│ ├── memory_safety.zig # Allocation/free tracking
│ ├── null_safety.zig # Optional unwrap checking
│ ├── variant_safety.zig # Tagged union field access
│ └── fd_safety.zig # File descriptor tracking
├── test/
│ ├── integration/ # BATS integration tests
│ │ ├── test_helper.bash
│ │ └── *.bats
│ └── cases/ # Test input files (.zig)
├── zig/ # Zig compiler submodule (instrumented fork)
├── build.zig # Build configuration
└── build.zig.zon # Package dependencies

Zig is a famously "unsafe" language. Memory management is done manually, which opens up the possibility of implementation errors. While Zig reduces security issues relative to C by eliminating out-of-bounds array access and null pointer dereferencing in safety-checked code, it is still less safe than Rust, which eliminates use-after-free, double-free, and data races through static analysis.
Inspired by Rust's MIRI project, CLR performs static analysis on Zig's AIR intermediate representation to achieve a higher degree of safety than Zig provides out-of-the-box. Unlike MIRI, which interprets Rust's MIR in a sandboxed pseudo-runtime, CLR transpiles AIR into Zig source code that statically runs analysis. Note that CLR's air output zig code could in principle be run at compile time, but by running through a zig intermediate, we produce an easy-to-understand and easy-to-debug logical flow. An ambitious person might use this general approach to emit a different output target, such as a proof assistant language, or refactor it to run entirely in the zig compiler!
The key insight: if you need MIRI for security-conscious Rust projects anyway, why not pick a simpler language and do MIRI-style analysis to get borrow checking and other refinement-type analysis? This project shows that such a future is a real possibility for Zig.
The Zig compilation pipeline is:
AIR is the ideal level for analysis because it's typed, interpretable as a "minimum viable" list of generalized programming instructions, and allows extending types with refinement metadata.
For an in-depth look at how Zig's AIR works, see Mitchell Hashimoto's blog post: https://mitchellh.com/zig/sema
MIT License - see LICENSE for details.
deinitallocatorstd.process.args,
std.mem.asBytes, std.HashMap, and allocator/file APIsstd.HashMap refinements with canonical metadata/key/value storage
identity across put, get, getPtr, and value iterationposix.open/close/dup/dup2/socket/accept/epoll_create/pipe tracking