Code map generator for Zig code.
  • Zig 98.9%
  • Shell 1.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Mikael Säker e941275f83 Add Zig standard library corpus check; fix two crashes it found
The hand-written fixtures only cover constructs someone thought to write
down. The Zig standard library is a much larger corpus that needs no
network and no vendored tree: it ships with the compiler and always matches
the Zig building this project.

Running it immediately found two hard crashes in writeNodeSource. Both
.unwrap_optional and .grouped_expression carry `node_and_token` data, but
the code read the `node` variant, so mapping any `expr.?` or `(expr)` in a
value position panicked on the union field access. No fixture produced
those shapes.

`zig build corpus` now maps all ~550 std lib files in both output formats
and asserts the output is well formed: no value rendered as a bare keyword,
no truncated signature, no declaration that lost its name. It runs on
testing.allocator, so it doubles as a leak check. Verified it fails when
either crash is reintroduced, while the fixture snapshots still pass.

Kept as a separate step from `test` so the common path stays fast:
`zig build test` is 0.3s, the corpus sweep is ~30s.

Also gitignore test/, the ad-hoc corpus copied in from another project.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 12:58:22 +02:00
src Add Zig standard library corpus check; fix two crashes it found 2026-07-25 12:58:22 +02:00
tests Fix API extraction bugs, add doc comments, JSON output, and tests 2026-07-25 11:43:18 +02:00
.gitignore Add Zig standard library corpus check; fix two crashes it found 2026-07-25 12:58:22 +02:00
build.zig Add Zig standard library corpus check; fix two crashes it found 2026-07-25 12:58:22 +02:00
CLAUDE.md Initial implementation of zigmap - Zig code map generator 2026-01-01 12:31:58 +01:00
LICENSE Add MIT license 2026-07-25 12:00:21 +02:00
README.md Add Zig standard library corpus check; fix two crashes it found 2026-07-25 12:58:22 +02:00

zigmap

Generates a code map of Zig source: the public API surface, with implementation details stripped. Built on std.zig.Ast, so what it reports is what the compiler parses.

Build

zig build          # binary at zig-out/bin/zigmap
zig build test     # snapshot tests (fast)
zig build corpus   # sweep the Zig standard library

Requires Zig 0.16.

Usage

zigmap <file.zig>...
zigmap <directory>...

  -h, --help    Show help
      --json    Emit JSON instead of Zig syntax

Directories are walked recursively for .zig files. Build outputs (zig-out/, zig-cache/) and hidden directories are skipped, and results are sorted by path so output is stable enough to diff across runs.

Exits non-zero if any input could not be read or parsed.

What it extracts

Only pub declarations, plus imports:

  • Module (//!) and declaration (///) doc comments
  • @import statements
  • Types — struct, enum, union, opaque — including packed/extern layout, enum backing types (enum(u8)), and union tag types (union(enum), union(Tag))
  • Function signatures, including extern/export/inline, callconv, generic parameters, and anonymous struct return types
  • Fields with their types, align, and default values; enum members with explicit tag values
  • Constants and variables with type, align, linksection, addrspace, and threadlocal

Function bodies are dropped. Initializer expressions are summarized: struct literals become .{...} and control-flow initializers (if, switch, block expressions) become ..., since their value is not part of the API contract.

Example

//! Editor configuration.

const std = @import("std");

/// How aggressively to retry.
pub const Config = struct {
    /// Display name.
    name: []const u8 = "default",
    retries: u32 = 3,
};

pub fn load(path: []const u8) !Config {
    // ...
}

fn internalHelper() void {}

maps to:

//! Editor configuration.

const std = @import("std");

/// How aggressively to retry.
pub const Config = struct {
    /// Display name.
    name: []const u8 = "default",
    retries: u32 = 3,
};
pub fn load(path: []const u8) !Config;

The output is Zig-flavored rather than compilable — bodyless pub fn f() void; and = ... are deliberate, so the map reads like a header.

JSON

--json emits the same information as a structured document:

{"files": [{
  "path": "config.zig",
  "doc": "Editor configuration.",
  "imports": [{"name": "std", "path": "std"}],
  "decls": [
    {"kind": "struct", "name": "Config", "doc": "How aggressively to retry.",
     "members": [
       {"kind": "field", "name": "name", "type": "[]const u8",
        "value": "\"default\"", "doc": "Display name."}
     ]},
    {"kind": "fn", "name": "load", "signature": "pub fn load(path: []const u8) !Config"}
  ]
}]}

Both formats are produced by the same traversal, so they cannot report different API surfaces.

Tests

zig build test — fast and hermetic. tests/fixtures/ holds inputs covering each construct; tests/snapshots/ holds the expected Zig and JSON output for each, compared byte-for-byte. Also checks that private declarations stay excluded and that malformed source raises an error rather than being skipped.

After an intentional output change, regenerate and review the diff:

zig build && zig build update-snapshots

zig build corpus — maps the Zig standard library (~550 files) in both formats and asserts no output is malformed: no value rendered as a bare keyword, no truncated signature, no declaration that lost its name. It runs on testing.allocator, so it doubles as a leak check.

The standard library is the corpus because it ships with the compiler: no network, no vendored tree, and it always matches the Zig building the project. Hand-written fixtures only cover constructs someone thought to write down — this sweep is what caught two node-data crashes (.unwrap_optional and .grouped_expression carry node_and_token, not node) that every fixture missed. It's slower, so it's a separate step from test; run it after touching the AST traversal.