{
  "zig": "0.17.0",
  "source": "docs/misra-zig-handbook.md",
  "rules": [
    {
      "id": "dir-4-1",
      "number": "4.1",
      "kind": "dir",
      "group": "Initialization, pointers, and unions",
      "obligation": "Runtime failure is a returned error, or a check that runs in every optimization mode. @setRuntimeSafety does not count. std.debug.assert and unreachable do not count either: in ReleaseFast and ReleaseSmall a false one is illegal behavior, so each states an assumption, not a check. orelse unreachable, .?, the AssumeCapacity methods, and @divExact, @shlExact, and @shrExact are assumptions in the same way. std.math.divExact and std.math.shlExact return errors instead. Some std.posix wrappers switch on errno with an => unreachable prong, so an errno the OS can return becomes illegal behavior; tools/std_lists.zig lists them, generated from the installed std. Those calls go through std.posix.system with a switch on std.posix.errno. The build that ships does not hard-code .ReleaseFast or .ReleaseSmall, and keeps sanitize_c on. An operation that updates two structures reserves capacity in both before the first change, so an error leaves both as they were.",
      "do": {
        "zig": [
          "fn boundedAdd(a: u32, b: u32) error{Overflow}!u32 {\n    return std.math.add(u32, a, b);\n}\n\nfn signedQuotient(a: i32, b: i32) error{ DivisionByZero, Overflow }!i32 {\n    return std.math.divTrunc(i32, a, b);\n}",
          "fn perItem(total: u32, count: u32) error{DivisionByZero}!u32 {\n    if (count == 0) return error.DivisionByZero;\n    return total / count;\n}",
          "fn priceOf(prices: *const std.StringHashMapUnmanaged(u32), name: []const u8) error{UnknownItem}!u32 {\n    return prices.get(name) orelse error.UnknownItem;\n}\n\nfn pushPair(gpa: std.mem.Allocator, list: *std.ArrayList(u32), a: u32, b: u32) !void {\n    try list.ensureUnusedCapacity(gpa, 2);\n    list.appendAssumeCapacity(a);\n    list.appendAssumeCapacity(b);\n}",
          "fn setReuse(fd: std.posix.socket_t) error{ InvalidOption, Unexpected }!void {\n    const one: c_int = 1;\n    const rc = std.posix.system.setsockopt(fd, std.posix.SOL.SOCKET, std.posix.SO.REUSEADDR, std.mem.asBytes(&one), @sizeOf(c_int));\n    return switch (std.posix.errno(rc)) {\n        .SUCCESS => {},\n        .INVAL, .NOPROTOOPT => error.InvalidOption,\n        else => error.Unexpected,\n    };\n}",
          "fn headerOf(bytes: []const u8) error{ Truncated, Misaligned }!*const Header {\n    if (bytes.len < @sizeOf(Header)) return error.Truncated;\n    if (!std.mem.isAligned(@intFromPtr(bytes.ptr), @alignOf(Header))) return error.Misaligned;\n    const aligned: [*]align(@alignOf(Header)) const u8 = @alignCast(bytes.ptr);\n    return @ptrCast(aligned);\n}"
        ],
        "note": "@divTrunc still overflows when the numerator is the minimum i32 and the divisor is -1. std.math.divTrunc returns error.Overflow for that pair and error.DivisionByZero for a zero divisor.\n\nUnsigned / cannot overflow, but a zero divisor is still illegal behavior. The divisor is checked in the same function:\n\nA lookup that can miss returns an error, and an AssumeCapacity call follows a reserve for every item it adds:\n\nA socket option set through the system call, with every errno an error:\n\nA C or untrusted byte slice is checked before a pointer cast. headerOf rejects a short buffer and a misaligned address, then @alignCast and @ptrCast."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "The safety switch deletes the overflow check on += in every build mode, including Debug. +% is the wrong sample: wrapping addition has no check to delete.",
            "zig": "fn bump(x: *u8) void {\n    @setRuntimeSafety(false);\n    x.* += 1;\n}"
          },
          {
            "mark": "compiles",
            "why": "std.debug.assert is if (!ok) unreachable. i comes from the C caller, and in ReleaseFast a false assert lets the read leave the buffer.",
            "zig": "export fn at(buf: [*]const u8, len: usize, i: usize) u8 {\n    std.debug.assert(i < len);\n    return buf[i];\n}"
          },
          {
            "mark": "compiles",
            "why": "A zero count panics in a safe build and is illegal behavior in ReleaseFast and ReleaseSmall.",
            "zig": "fn perItemUnchecked(total: u32, count: u32) u32 {\n    return total / count;\n}"
          },
          {
            "mark": "compiles",
            "why": ".? is orelse unreachable. A name that is not in the map panics in a safe build and is illegal behavior in ReleaseFast and ReleaseSmall.",
            "zig": "fn priceUnchecked(prices: *const std.StringHashMapUnmanaged(u32), name: []const u8) u32 {\n    return prices.get(name).?;\n}"
          },
          {
            "mark": "compiles",
            "why": "The reserve is for one item and two are appended. ensureUnusedCapacity may grow by more than it was asked, so a test can pass. With initCapacity(gpa, 1) the second append fails the capacity assertion in Debug, and in ReleaseFast it is illegal behavior.",
            "zig": "fn pushShort(gpa: std.mem.Allocator, list: *std.ArrayList(u32), a: u32, b: u32) !void {\n    try list.ensureUnusedCapacity(gpa, 1);\n    list.appendAssumeCapacity(a);\n    list.appendAssumeCapacity(b);\n}"
          },
          {
            "mark": "compiles",
            "why": "In Zig 0.17.0 std.posix.setsockopt maps EINVAL to unreachable, so an option the kernel refuses is illegal behavior in ReleaseFast.",
            "zig": "fn setReuseAssumed(fd: std.posix.socket_t) !void {\n    const one: c_int = 1;\n    try std.posix.setsockopt(fd, std.posix.SOL.SOCKET, std.posix.SO.REUSEADDR, std.mem.asBytes(&one));\n}"
          },
          {
            "mark": "compiles",
            "why": "The build that ships has no safety checks at all, whatever -Doptimize says.",
            "zig": "pub fn build(b: *std.Build) void {\n    const exe = b.addExecutable(.{\n        .name = \"app\",\n        .root_module = b.createModule(.{\n            .root_source_file = b.path(\"src/main.zig\"),\n            .target = b.graph.host,\n            .optimize = .ReleaseFast,\n        }),\n    });\n    b.installArtifact(exe);\n}"
          }
        ]
      }
    },
    {
      "id": "dir-4-6",
      "number": "4.6",
      "kind": "dir",
      "group": "Types, portability, and arithmetic",
      "obligation": "A numeric value that means a size, a count, or a wire field names its width and its signedness. usize is for lengths and indices. c_int and c_long appear only where the C header says int or long.",
      "do": {
        "zig": [
          "pub const block_size: u32 = 512;",
          "const Header = extern struct {\n    magic: u32,\n    size: u64,\n};\n\ncomptime {\n    std.debug.assert(@sizeOf(Header) == 16);\n    std.debug.assert(@alignOf(Header) == 8);\n}"
        ],
        "note": "Header is an extern struct because it has to match a C header. On the x86_64 System V ABI the u64 forces 4 bytes of padding, so the size is 16 and the alignment is 8. On a target whose C alignment of a 64-bit integer is 4, the same struct is 12 bytes and the assert fails. A file format is an explicit decode from []u8 with a stated endianness. packed struct has the ABI of its backing integer, so its byte order is the target endianness, and a field cannot be an array. extern padding follows the target C ABI, so it is not a file format either."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "comptime_int has no width. A later use can be 32 bits on one call and 64 on the next.",
            "zig": "pub const block_size = 512;"
          },
          {
            "mark": "compiles",
            "why": "still the wrong width. A u32 field in a header is not a usize.",
            "zig": "const Disk = extern struct {\n    blocks: usize,\n};"
          }
        ]
      }
    },
    {
      "id": "dir-4-7",
      "number": "4.7",
      "kind": "dir",
      "group": "Functions and linkage",
      "obligation": "Failure and not-found are reported as an error or as ?T, and the caller handles them. An in-band value (-1, std.math.maxInt, an empty slice that means \"absent\") is not used, except where a C ABI requires it.",
      "do": {
        "zig": [
          "fn find(items: []const u32, x: u32) ?usize {\n    for (items, 0..) |item, i| {\n        if (item == x) return i;\n    }\n    return null;\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "A caller that forgets the test indexes with maxInt(usize), which is past the end of any slice.",
            "zig": "fn findOrMax(items: []const u32, x: u32) usize {\n    for (items, 0..) |item, i| {\n        if (item == x) return i;\n    }\n    return std.math.maxInt(usize);\n}"
          }
        ]
      }
    },
    {
      "id": "dir-4-11",
      "number": "4.11",
      "kind": "dir",
      "group": "Functions and linkage",
      "obligation": "A value passed to a std function meets that function's preconditions, and the caller checks it first. Many std functions assert an argument instead of returning an error, and in ReleaseFast a false assert is illegal behavior. A doc comment that says \"Asserts\" names such a precondition, but not every one is named: std.math.log2_int asserts x != 0 in its body and its doc comment does not say so. Read the body before passing a std function a value from outside.",
      "do": {
        "zig": [
          "fn bitWidth(x: u32) error{Zero}!u5 {\n    if (x == 0) return error.Zero;\n    return std.math.log2_int(u32, x);\n}\n\nfn removeAt(list: *std.ArrayList(u32), i: usize) error{OutOfRange}!u32 {\n    if (i >= list.items.len) return error.OutOfRange;\n    return list.orderedRemove(i);\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "log2_int asserts that x is not zero. A zero panicked with reached unreachable code in a 0.17.0 Debug run and crashed a ReleaseFast run.",
            "zig": "fn bitWidthUnchecked(x: u32) u5 {\n    return std.math.log2_int(u32, x);\n}"
          },
          {
            "mark": "compiles",
            "why": "orderedRemove asserts that the index is in bounds. An index past the end panicked in a 0.17.0 Debug run and crashed a ReleaseFast run.",
            "zig": "fn removeUnchecked(list: *std.ArrayList(u32), i: usize) u32 {\n    return list.orderedRemove(i);\n}"
          }
        ]
      }
    },
    {
      "id": "dir-4-12",
      "number": "4.12",
      "kind": "dir",
      "group": "Memory and other resources",
      "obligation": "Allocation goes through an Allocator argument. The function that allocates either frees on every path or returns the memory and documents that the caller owns it. The std.Io is a parameter too: only main builds a std.Io.Threaded (or takes init.io), because Threaded.init installs process-wide signal handlers.",
      "do": {
        "zig": [
          "fn take(allocator: std.mem.Allocator, n: usize) ![]u8 {\n    const buf = try allocator.alloc(u8, n);\n    errdefer allocator.free(buf);\n    @memset(buf, 0);\n    return buf;\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "A library that allocates from std.heap.c_allocator or std.heap.page_allocator hides the owner. The caller cannot use one allocator for the whole call, and a test cannot see the leak.",
            "zig": "fn hidden(n: usize) ![]u8 {\n    return std.heap.page_allocator.alloc(u8, n);\n}"
          },
          {
            "mark": "compiles",
            "why": "Every call builds an std.Io and installs signal handlers for the whole process.",
            "zig": "fn logLine(msg: []const u8) !void {\n    var threaded: std.Io.Threaded = .init(std.heap.page_allocator, .{});\n    defer threaded.deinit();\n    try std.Io.File.stdout().writeStreamingAll(threaded.io(), msg);\n}"
          }
        ]
      }
    },
    {
      "id": "dir-4-14",
      "number": "4.14",
      "kind": "dir",
      "group": "Buffers and strings",
      "obligation": "A value from outside (a file, a socket, a C caller) is checked before it sizes an allocation, a read, or a loop. A length field is compared with a named maximum first. A read limit is .limited(max), never .unlimited. A list or map that grows once per request, peer, or loop pass has a named cap and returns an error at it.",
      "do": {
        "zig": [
          "const max_frame_bytes: u32 = 64 * 1024;\nconst max_config_bytes: usize = 64 * 1024;\n\nfn frameBody(gpa: std.mem.Allocator, len: u32) ![]u8 {\n    if (len > max_frame_bytes) return error.FrameTooLarge;\n    return gpa.alloc(u8, len);\n}\n\nfn readConfig(dir: std.Io.Dir, io: std.Io, gpa: std.mem.Allocator) ![]u8 {\n    return dir.readFileAlloc(io, \"config\", gpa, .limited(max_config_bytes));\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "The file chooses how much memory this allocates.",
            "zig": "fn readConfigUnbounded(dir: std.Io.Dir, io: std.Io, gpa: std.mem.Allocator) ![]u8 {\n    return dir.readFileAlloc(io, \"config\", gpa, .unlimited);\n}"
          }
        ]
      }
    },
    {
      "id": "dir-4-15",
      "number": "4.15",
      "kind": "dir",
      "group": "Types, portability, and arithmetic",
      "obligation": "A float computed from outside data is checked with std.math.isFinite before it is stored, compared, or converted. A zero divisor or an overflow gives an infinity, 0.0 / 0.0 gives NaN, and every comparison with NaN is false, so a range check such as x > limit lets NaN through. @setFloatMode(.optimized) is not used: in that mode the compiler may assume no operand or result is NaN or infinite, and the language reference calls the result undefined. A float that becomes an integer also follows Rule 10.3.",
      "do": {
        "zig": [
          "fn mean(sum: f64, count: u32) error{ Empty, NotFinite }!f64 {\n    if (count == 0) return error.Empty;\n    const m = sum / @as(f64, @floatFromInt(count));\n    if (!std.math.isFinite(m)) return error.NotFinite;\n    return m;\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "A NaN reading is never over the limit. In a 0.17.0 run, a NaN compared false with < 1.0 and with >= 1.0.",
            "zig": "fn overLimit(reading: f64, limit: f64) bool {\n    return reading > limit;\n}"
          },
          {
            "mark": "compiles",
            "why": ".optimized lets the compiler assume b is not zero and the quotient is finite. A zero b panicked with division by zero in a 0.17.0 Debug run. ReleaseFast has no check.",
            "zig": "fn ratio(a: f64, b: f64) f64 {\n    @setFloatMode(.optimized);\n    return a / b;\n}"
          }
        ]
      }
    },
    {
      "id": "dir-5-1",
      "number": "5.1",
      "kind": "dir",
      "group": "Threads, locks, and atomics",
      "obligation": "Data written by one thread and read by another goes through std.Io.Mutex or std.atomic.Value. A plain variable shared between threads is a data race, which is illegal behavior.",
      "do": {
        "zig": [
          "fn work(hits: *std.atomic.Value(u64)) void {\n    _ = hits.fetchAdd(1, .seq_cst);\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "Two workers that call countHit race on hits.",
            "zig": "var hits: u64 = 0;\n\nfn countHit() void {\n    hits += 1;\n}"
          }
        ]
      }
    },
    {
      "id": "dir-5-2",
      "number": "5.2",
      "kind": "dir",
      "group": "Threads, locks, and atomics",
      "obligation": "Threads do not deadlock. A function that holds two mutexes takes them in one documented order, and every function that takes both uses that order. A thread does not lock a mutex it already holds: std.Io.Mutex is not recursive.",
      "do": {
        "zig": [
          "const Account = struct {\n    id: u32,\n    balance: u64,\n    mu: std.Io.Mutex = .init,\n};\n\n/// Takes the two locks in ascending `id` order, the order every caller uses.\nfn transfer(io: std.Io, from: *Account, to: *Account, amount: u64) !void {\n    if (from.id == to.id) return error.SameAccount;\n    const first = if (from.id < to.id) from else to;\n    const second = if (from.id < to.id) to else from;\n    try first.mu.lock(io);\n    defer first.mu.unlock(io);\n    try second.mu.lock(io);\n    defer second.mu.unlock(io);\n    const debited = try std.math.sub(u64, from.balance, amount);\n    const credited = try std.math.add(u64, to.balance, amount);\n    from.balance = debited;\n    to.balance = credited;\n}"
        ],
        "note": "transfer computes both balances before it writes either, so an error leaves both accounts as they were (Dir 4.1)."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "One thread calls lockBoth(io, &x, &y) and another calls lockBoth(io, &y, &x). Each can take its first mutex and then wait forever for the other's.",
            "zig": "fn lockBoth(io: std.Io, a: *std.Io.Mutex, b: *std.Io.Mutex) !void {\n    try a.lock(io);\n    defer a.unlock(io);\n    try b.lock(io);\n    defer b.unlock(io);\n}"
          },
          {
            "mark": "compiles",
            "why": "The second lock waits for the first, which this thread holds. A 0.17.0 run hung in Debug and in ReleaseFast.",
            "zig": "fn relock(io: std.Io, mu: *std.Io.Mutex) !void {\n    try mu.lock(io);\n    defer mu.unlock(io);\n    try mu.lock(io);\n    defer mu.unlock(io);\n}"
          }
        ]
      }
    },
    {
      "id": "dir-5-3",
      "number": "5.3",
      "kind": "dir",
      "group": "Threads, locks, and atomics",
      "obligation": "Threads are created at start-up, a fixed number of them, and every one is joined once. No thread is spawned per request or inside a while loop, and none is detached. A second join of the same std.Thread segfaulted a 0.17.0 run in Debug and in ReleaseFast.",
      "do": {
        "zig": [
          "const max_workers: usize = 4;\n\nfn work(hits: *std.atomic.Value(u64)) void {\n    _ = hits.fetchAdd(1, .seq_cst);\n}\n\npub fn main() !void {\n    var hits: std.atomic.Value(u64) = .init(0);\n    var workers: [max_workers]std.Thread = undefined;\n    var started: usize = 0;\n    defer for (workers[0..started]) |w| w.join();\n    for (&workers) |*w| {\n        w.* = try std.Thread.spawn(.{}, work, .{&hits});\n        started += 1;\n    }\n}"
        ],
        "note": "started counts the threads that exist, so a failed spawn joins only those."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "One thread per connection, never joined: the count follows the input, and nothing waits for them to finish.",
            "zig": "fn serveAll(conns: []const u32) !void {\n    for (conns) |conn| {\n        const t = try std.Thread.spawn(.{}, handle, .{conn});\n        t.detach();\n    }\n}\n\nfn handle(conn: u32) void {\n    _ = conn;\n}"
          }
        ]
      }
    },
    {
      "id": "rule-1-5",
      "number": "1.5",
      "kind": "rule",
      "group": "Functions and linkage",
      "obligation": "A declaration the Zig 0.17 standard library marks deprecated is not used. misra-lint checks every one, from a list generated out of the installed std (tools/std_lists.zig).",
      "do": {
        "zig": [
          "fn colon(s: []const u8) ?usize {\n    return std.mem.findScalar(u8, s, ':');\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "indexOfScalar is deprecated in favor of findScalar, and a later release may remove it.",
            "zig": "fn colonOld(s: []const u8) ?usize {\n    return std.mem.indexOfScalar(u8, s, ':');\n}"
          }
        ]
      }
    },
    {
      "id": "rule-2-1",
      "number": "2.1",
      "kind": "rule",
      "group": "Control flow",
      "obligation": "Every source file is reachable from the test root through @import. Zig analyzes only what is referenced, so a file nothing imports is never type-checked. misra-lint --root ROOT FILE... reports each FILE the root does not reach.",
      "do": {
        "zig": [],
        "note": "The test root names every file: test { _ = @import(\"parser.zig\"); }."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "A parser.zig that nothing imports is never analyzed, so the tests pass while it holds a type error.",
            "zig": ""
          }
        ]
      }
    },
    {
      "id": "rule-8-1",
      "number": "8.1",
      "kind": "rule",
      "group": "Types, portability, and arithmetic",
      "obligation": "The type is written on a container-level declaration, and on any declaration whose value is a literal. A local may take its type from a typed expression: a call, a typed name, or an operation on typed values.",
      "do": {
        "zig": [
          "pub const block_size: u32 = 512;\n\nfn blocksFor(bytes: u64) u64 {\n    const whole = bytes / block_size;\n    return whole + @intFromBool(bytes % block_size != 0);\n}"
        ],
        "note": "whole is inferred, and its type is the u64 of bytes, named on the line above."
      },
      "dont": {
        "cases": [
          {
            "mark": "rejected",
            "why": "A var with a literal and no type is comptime_int, which has no runtime width.",
            "zig": "export fn f() u32 {\n    var n = 0;\n    n += 1;\n    return n;\n}"
          },
          {
            "mark": "compiles",
            "why": "Dropping the type on a public constant is the same hole as a bare int in C. See Dir 4.6.",
            "zig": ""
          }
        ]
      }
    },
    {
      "id": "rule-8-2",
      "number": "8.2",
      "kind": "rule",
      "group": "Functions and linkage",
      "obligation": "A function has a prototype: every parameter has a name and a concrete type. anytype is for a generic whose body rejects a bad type at compile time. It is not how an export function talks to C.",
      "do": {
        "zig": [
          "export fn diskSize(blocks: u32, block_size_bytes: u32, out: ?*u32) c_int {\n    const dest = out orelse return 1;\n    dest.* = std.math.mul(u32, blocks, block_size_bytes) catch return 1;\n    return 0;\n}"
        ],
        "note": "The status is c_int because the header says int. The sizes stay u32 because the header says uint32_t. out is optional because a C caller can pass null. Null returns 1 and is not dereferenced."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "The names say nothing, and anytype will accept a u64 offset at one call site and a pointer at another.",
            "zig": "fn readAt(a: anytype, b: anytype) void {\n    _ = a;\n    _ = b;\n}"
          }
        ]
      }
    },
    {
      "id": "rule-8-4",
      "number": "8.4",
      "kind": "rule",
      "group": "Functions and linkage",
      "obligation": "An export function has one signature, and it is the signature in the header. Callers inside Zig call the pub function. There is no second handwritten prototype. A C function comes from the header translated in build.zig (addTranslateC); a handwritten extern fn is for an ABI no header exposes, with a comment that names it.",
      "do": {
        "zig": [],
        "note": "diskSize above is the declaration and the definition."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "The comment says uint32_t. The parameter is usize. On a 64-bit target those are different types, and the C caller will pass 32 bits.",
            "zig": "// uint32_t blocks\nexport fn diskBlocks(blocks: usize) void {\n    _ = blocks;\n}"
          },
          {
            "mark": "compiles",
            "why": "The declaration is a second copy of unistd.h. If the header changes, nothing compares the two.",
            "zig": "extern \"c\" fn write(fd: c_int, buf: [*]const u8, count: usize) isize;"
          }
        ]
      }
    },
    {
      "id": "rule-8-7",
      "number": "8.7",
      "kind": "rule",
      "group": "Functions and linkage",
      "obligation": "A function used in one file is not pub.",
      "do": {
        "zig": [],
        "note": "boundedAdd, take, and sumTree have no pub. A test in the same file can call them. Another file cannot."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "pub on a helper used only in this file makes it visible to other Zig files. It does not create a linker symbol. export does that. It becomes API, and a later edit has to keep it stable.",
            "zig": "pub fn helperOnlyThisFileUses(n: u32) u32 {\n    return n + 1;\n}"
          }
        ]
      }
    },
    {
      "id": "rule-8-8",
      "number": "8.8",
      "kind": "rule",
      "group": "Functions and linkage",
      "obligation": "Internal linkage is the absence of pub and export on that one declaration. Do not add a public alias that publishes it again.",
      "do": {
        "zig": [
          "fn checksum(data: []const u8) u8 {\n    var sum: u8 = 0;\n    for (data) |b| sum +%= b;\n    return sum;\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "The wrapper publishes a file-local function to other Zig files. pub is visibility, not a second linker symbol.",
            "zig": "fn checksum(data: []const u8) u8 {\n    _ = data;\n    return 0;\n}\n\npub fn checksumPub(data: []const u8) u8 {\n    return checksum(data);\n}"
          }
        ]
      }
    },
    {
      "id": "rule-8-13",
      "number": "8.13",
      "kind": "rule",
      "group": "Initialization, pointers, and unions",
      "obligation": "A pointer or a slice that the function does not write is const.",
      "do": {
        "zig": [
          "fn checksum(data: []const u8) u8 {\n    var sum: u8 = 0;\n    for (data) |b| sum +%= b;\n    return sum;\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "[]u8 says the function may write. A caller with []const u8 cannot pass the data, and a caller who can will not know whether the buffer comes back changed.",
            "zig": "fn checksum(data: []u8) u8 {\n    var sum: u8 = 0;\n    for (data) |b| sum +%= b;\n    return sum;\n}"
          }
        ]
      }
    },
    {
      "id": "rule-8-14",
      "number": "8.14",
      "kind": "rule",
      "group": "Initialization, pointers, and unions",
      "obligation": "noalias is not used. It promises the compiler that no other pointer reaches the same memory during the call, and nothing checks the promise in any build mode. A caller that breaks it gets a result that changes with the optimization mode.",
      "do": {
        "zig": [
          "fn addTwice(total: *u32, step: *const u32) error{Overflow}!void {\n    total.* = try std.math.add(u32, total.*, step.*);\n    total.* = try std.math.add(u32, total.*, step.*);\n}"
        ],
        "note": "addTwice(&x, &x) is defined: the second read of step sees the first write."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "Called as addTwiceNoalias(&x, &x) with x at 1, a 0.17.0 Debug build gave 4, and ReleaseSafe and ReleaseFast gave 3.",
            "zig": "fn addTwiceNoalias(noalias total: *u32, noalias step: *const u32) error{Overflow}!void {\n    total.* = try std.math.add(u32, total.*, step.*);\n    total.* = try std.math.add(u32, total.*, step.*);\n}"
          }
        ]
      }
    },
    {
      "id": "rule-9-1",
      "number": "9.1",
      "kind": "rule",
      "group": "Initialization, pointers, and unions",
      "obligation": "undefined may be stored. It may not be read. allocator.alloc returns undefined bytes. Write them, or @memset, before a read. Wipe a secret with std.crypto.secureZero. A plain @memset of a dead buffer can be deleted.",
      "do": {
        "zig": [
          "fn take(allocator: std.mem.Allocator, n: usize) ![]u8 {\n    const buf = try allocator.alloc(u8, n);\n    errdefer allocator.free(buf);\n    @memset(buf, 0);\n    return buf;\n}\n\nfn wipe(buf: *[32]u8) void {\n    std.crypto.secureZero(u8, buf);\n}"
        ],
        "note": "The stack in sumTree is undefined and the first write is stack[0] = root before any read. Each later read is a slot written on an earlier iteration."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "flag == false returns an unread u32. Safe builds do not always catch this.",
            "zig": "fn value(flag: bool) u32 {\n    var x: u32 = undefined;\n    if (flag) x = 1;\n    return x;\n}"
          }
        ]
      }
    },
    {
      "id": "rule-9-2",
      "number": "9.2",
      "kind": "rule",
      "group": "Initialization, pointers, and unions",
      "obligation": "Every field of a struct literal is written. An array is a full value, @memset, or std.mem.zeroes. There is no C {0} that fills the tail.",
      "do": {
        "zig": [
          "const root = Node{ .value = 1, .left = null, .right = &right };\nvar raw align(8) = std.mem.zeroes([16]u8);"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "size is never stored. The return reads undefined padding and an undefined field.",
            "zig": "fn blank() Header {\n    var header: Header = undefined;\n    header.magic = 1;\n    return header;\n}"
          }
        ]
      }
    },
    {
      "id": "rule-10-1",
      "number": "10.1",
      "kind": "rule",
      "group": "Types, portability, and arithmetic",
      "obligation": "An operator gets an operand of the right kind. and and or take bool. A shift amount is an unsigned integer in range. A boolean is not an integer, and an integer is not a condition.",
      "do": {
        "zig": [
          "fn both(ready: bool, allowed: bool) bool {\n    return ready and allowed;\n}\n\nfn shift(value: u32, amount: u32) error{Overflow}!u32 {\n    if (amount >= @bitSizeOf(u32)) return error.Overflow;\n    return value << @as(std.math.Log2Int(u32), @intCast(amount));\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "rejected",
            "why": "if does not take a u32.",
            "zig": "export fn f(n: u32) u32 {\n    if (n) return 1;\n    return 0;\n}"
          },
          {
            "mark": "rejected",
            "why": "i32 and u32 do not add.",
            "zig": "export fn f(a: i32, b: u32) u32 {\n    return a + b;\n}"
          },
          {
            "mark": "compiles",
            "why": "amount from the caller goes straight into @intCast. 32 or more panics in a safe build and is illegal behavior in ReleaseFast and ReleaseSmall. The do checks it against @bitSizeOf first.",
            "zig": "fn shiftUnchecked(value: u32, amount: u32) u32 {\n    return value << @intCast(amount);\n}"
          }
        ]
      }
    },
    {
      "id": "rule-10-2",
      "number": "10.2",
      "kind": "rule",
      "group": "Types, portability, and arithmetic",
      "obligation": "A byte that means a character is not incremented or decremented as if the encoding were arithmetic. Convert a small integer to a digit only after the value is in range.",
      "do": {
        "zig": [
          "fn decimalDigit(n: u8) error{Overflow}!u8 {\n    if (n > 9) return error.Overflow;\n    return '0' + n;\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "c + 1 is the next integer, not the next character. At 255 it panics in a safe build and is illegal behavior in ReleaseFast and ReleaseSmall.",
            "zig": "fn nextByte(c: u8) u8 {\n    return c + 1;\n}"
          }
        ]
      }
    },
    {
      "id": "rule-10-3",
      "number": "10.3",
      "kind": "rule",
      "group": "Types, portability, and arithmetic",
      "obligation": "A value from outside the function is not forced into a narrower type or a different signedness. std.math.cast returns null when it does not fit. @truncate means the low bits are the result you want. @bitCast reinterprets the same width. It is not a conversion. A float becomes an integer (@trunc, @floor, @ceil, or @round with an integer result) only after a check that it is finite and in range. @intFromFloat is deprecated in 0.17 and means @trunc.",
      "do": {
        "zig": [
          "fn narrow(wide: u32) error{Overflow}!u16 {\n    return std.math.cast(u16, wide) orelse error.Overflow;\n}\n\nfn toMillis(seconds: f64) error{OutOfRange}!u32 {\n    const ms = seconds * 1000.0;\n    if (!(ms >= 0.0 and ms < 4294967296.0)) return error.OutOfRange;\n    return @trunc(ms);\n}"
        ],
        "note": "NaN fails both comparisons, so it takes the error path."
      },
      "dont": {
        "cases": [
          {
            "mark": "rejected",
            "why": "The implicit narrowing is the C bug.",
            "zig": "export fn f(a: u32) u16 {\n    return a;\n}"
          },
          {
            "mark": "compiles",
            "why": "@truncate silences that error and keeps the low 16 bits. 0x1_0000 becomes 0.",
            "zig": "export fn f(a: u32) u16 {\n    return @truncate(a);\n}"
          },
          {
            "mark": "compiles",
            "why": "@intCast asserts that the value fits. With no check in this function, 70000 panics in a safe build and is illegal behavior in ReleaseFast and ReleaseSmall.",
            "zig": "fn narrowUnchecked(wide: u32) u16 {\n    return @intCast(wide);\n}"
          },
          {
            "mark": "compiles",
            "why": "5e9 seconds panics in a safe build. In a Zig 0.17.0 Debug run NaN gave 0 with no panic, so a safe build does not catch every bad value. In ReleaseFast both are illegal behavior.",
            "zig": "fn toMillisUnchecked(seconds: f64) u32 {\n    return @trunc(seconds * 1000.0);\n}"
          }
        ]
      }
    },
    {
      "id": "rule-10-4",
      "number": "10.4",
      "kind": "rule",
      "group": "Types, portability, and arithmetic",
      "obligation": "The two operands of an arithmetic operator have the same signedness. Zig rejects i32 + u32. It accepts i32 + u16, because a u16 coerces to i32 and the add is an i32 add. Do not bitcast past the rejection.",
      "do": {
        "zig": [
          "fn boundedAdd(a: u32, b: u32) error{Overflow}!u32 {\n    return std.math.add(u32, a, b);\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "rejected",
            "why": "",
            "zig": "export fn f(a: i32, b: u32) u32 {\n    return a + b;\n}"
          },
          {
            "mark": "compiles",
            "why": "@bitCast makes a negative i32 look like a large u32, then the add proceeds.",
            "zig": "export fn f(a: i32, b: u32) u32 {\n    return @as(u32, @bitCast(a)) + b;\n}"
          }
        ]
      }
    },
    {
      "id": "rule-10-5",
      "number": "10.5",
      "kind": "rule",
      "group": "Types, portability, and arithmetic",
      "obligation": "A bool stays a bool. An enum stays an enum until the bytes hit the wire. @backingInt is that boundary, and the result is not fed back into arithmetic to mean \"the next tag\". The way back from the wire is std.enums.fromInt, which returns null for a value with no tag. @fromBackingInt of such a value is illegal behavior; zig fmt rewrites the older @enumFromInt to it. A non-exhaustive enum (_) accepts every value of its backing type, so fromInt never returns null for it and the switch handles the unknown values with a _ prong.",
      "do": {
        "zig": [
          "const Opcode = enum(u8) { load = 1, store = 2 };\n\nfn opcodeByte(op: Opcode) u8 {\n    return @backingInt(op);\n}",
          "fn decode(byte: u8) error{Unknown}!Opcode {\n    return std.enums.fromInt(Opcode, byte) orelse error.Unknown;\n}"
        ],
        "note": "zig fmt writes @backingInt. That is the 0.17 builtin for the enum's integer value."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "Adding 1 to the tag invents a value the enum may not have.",
            "zig": "fn nextOp(op: Opcode) u8 {\n    return @backingInt(op) + 1;\n}"
          },
          {
            "mark": "compiles",
            "why": "Byte 9 has no tag. @fromBackingInt panics in a safe build and is illegal behavior in ReleaseFast and ReleaseSmall.",
            "zig": "fn rawOpcode(byte: u8) Opcode {\n    return @fromBackingInt(byte);\n}"
          }
        ]
      }
    },
    {
      "id": "rule-10-6",
      "number": "10.6",
      "kind": "rule",
      "group": "Types, portability, and arithmetic",
      "obligation": "Widen the operands, then add. A u16 addition assigned to a u32 still happens at u16.",
      "do": {
        "zig": [
          "fn sumWide(a: u16, b: u16) error{Overflow}!u32 {\n    return std.math.add(u32, a, b);\n}"
        ],
        "note": "sumWide(65535, 2) is 65537. The u16 arguments convert to u32 at the call, and the add is 32 bits."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "a + b is a u16 add. In a safe build, 65535 + 2 panics. In ReleaseFast and ReleaseSmall the add is illegal behavior. The u32 return does not make that result 1. Specified wrap is +%.",
            "zig": "export fn f(a: u16, b: u16) u32 {\n    return a + b;\n}"
          }
        ]
      }
    },
    {
      "id": "rule-10-7",
      "number": "10.7",
      "kind": "rule",
      "group": "Types, portability, and arithmetic",
      "obligation": "A composite expression is not the operand of a wider arithmetic operation. Finish the inner operation at the wider width first.",
      "do": {
        "zig": [
          "fn scale(a: u16, b: u16, c: u32) error{Overflow}!u32 {\n    const sum = try sumWide(a, b);\n    return std.math.mul(u32, sum, c);\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "(a + b) is a u16 add, before the multiply. In a safe build that add panics on overflow. In ReleaseFast and ReleaseSmall it is illegal behavior, so there is no defined wrapped product.",
            "zig": "export fn f(a: u16, b: u16, c: u32) u32 {\n    return (a + b) * c;\n}"
          }
        ]
      }
    },
    {
      "id": "rule-10-8",
      "number": "10.8",
      "kind": "rule",
      "group": "Types, portability, and arithmetic",
      "obligation": "A cast is not how you widen a sum. Casting the sum casts the narrow result after the add. In a safe build that add panics. In ReleaseFast and ReleaseSmall the add is illegal behavior. Widen the inputs, as in Rule 10.6.",
      "do": {
        "zig": [],
        "note": "sumWide."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "@as(u32, a + b) still adds at u16, then widens whatever came out.",
            "zig": "export fn f(a: u16, b: u16) u32 {\n    return @as(u32, a + b);\n}"
          }
        ]
      }
    },
    {
      "id": "rule-11-3",
      "number": "11.3",
      "kind": "rule",
      "group": "Initialization, pointers, and unions",
      "obligation": "A pointer to one object type is not cast to a pointer to another. A value is reinterpreted with @bitCast. The one overlay is a struct on checked bytes, headerOf under Dir 4.1.",
      "do": {
        "zig": [
          "fn bitsOf(x: f32) u32 {\n    return @bitCast(x);\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "It reads an f32 through a *const u32. @bitCast states the same reinterpretation without a pointer.",
            "zig": "fn bitsByPointer(x: *const f32) u32 {\n    const p: *const u32 = @ptrCast(x);\n    return p.*;\n}"
          }
        ]
      }
    },
    {
      "id": "rule-11-8",
      "number": "11.8",
      "kind": "rule",
      "group": "Initialization, pointers, and unions",
      "obligation": "@constCast and @volatileCast are not used. A function that writes takes []T or *T, and one that only reads takes []const T or *const T (Rule 8.13). Where a C header leaves const off a parameter that the C function only reads, the cast sits at that one call, with an allow comment that names the header.",
      "do": {
        "zig": [
          "fn upperInto(out: []u8, text: []const u8) error{NoSpaceLeft}![]u8 {\n    if (out.len < text.len) return error.NoSpaceLeft;\n    for (out[0..text.len], text) |*o, c| o.* = std.ascii.toUpper(c);\n    return out[0..text.len];\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "limit can point at a constant. In a 0.17.0 run, raise(&max_retries) on const max_retries: u32 = 10 left max_retries reading 10 in Debug, ReleaseSafe, and ReleaseFast. The write was lost, and nothing reported it.",
            "zig": "fn raise(limit: *const u32) void {\n    const m = @constCast(limit);\n    m.* = 99;\n}"
          }
        ]
      }
    },
    {
      "id": "rule-12-1",
      "number": "12.1",
      "kind": "rule",
      "group": "Control flow",
      "obligation": "Parenthesize a mix of and and or. Parenthesize a mix of shifts, bitwise operators, and arithmetic.",
      "do": {
        "zig": [
          "fn pack(n: u32, tag: u32) u32 {\n    return (n << 4) | (tag & 0xF);\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "and it is a different value. &, ^, and | share one precedence level and associate left. << is tighter. So the line below is ((n << 4) | tag) & 0xF. pack(1, 3) is then 3. The parenthesized do is (1 << 4) | (3 & 0xF), which is 19 (0x13).",
            "zig": "fn pack(n: u32, tag: u32) u32 {\n    return n << 4 | tag & 0xF;\n}"
          }
        ]
      }
    },
    {
      "id": "rule-13-4",
      "number": "13.4",
      "kind": "rule",
      "group": "Control flow",
      "obligation": "Assignment is a statement. The condition is a separate expression.",
      "do": {
        "zig": [
          "fn storedFlag() bool {\n    var n: u32 = 0;\n    n = 1;\n    if (n != 0) return true;\n    return false;\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "rejected",
            "why": "",
            "zig": "export fn f() void {\n    var x: u32 = 0;\n    if (x = 1) {}\n}"
          }
        ]
      }
    },
    {
      "id": "rule-14-4",
      "number": "14.4",
      "kind": "rule",
      "group": "Control flow",
      "obligation": "The condition of if and while is a bool, an optional, or an error union. for walks a slice, an array, or a range, and a bare integer is rejected. Compare an integer before it is a bound or a condition.",
      "do": {
        "zig": [
          "fn nonEmpty(n: u32) bool {\n    return n != 0;\n}",
          "fn lenOf(p: ?[]const u8) usize {\n    const s = p orelse return 0;\n    return s.len;\n}"
        ],
        "note": "An optional unwrap is a condition of optional type, which is the Zig form of a null test:"
      },
      "dont": {
        "cases": [
          {
            "mark": "rejected",
            "why": "for takes a slice, an array, or a range, and n is none of them. Walk 0..n. if (n) on a u32 is the same error, shown under Rule 10.1.",
            "zig": "export fn f(n: usize) void {\n    for (n) |_| {}\n}"
          }
        ]
      }
    },
    {
      "id": "rule-15-1",
      "number": "15.1",
      "kind": "rule",
      "group": "Control flow",
      "obligation": "There is no goto. Cleanup is defer and errdefer. A retry has a bound and returns an error when the bound is hit. One cleanup path, not a jump back to an earlier label.",
      "do": {
        "zig": [
          "fn take(allocator: std.mem.Allocator, n: usize) ![]u8 {\n    const buf = try allocator.alloc(u8, n);\n    errdefer allocator.free(buf);\n    @memset(buf, 0);\n    return buf;\n}",
          "fn connect(dial: *const fn () error{Busy}!void) error{GaveUp}!void {\n    for (0..4) |_| {\n        dial() catch continue;\n        return;\n    }\n    return error.GaveUp;\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "continue back to while (true) is a backward jump with no bound.",
            "zig": "fn connect(dial: *const fn () error{Busy}!void) !void {\n    while (true) {\n        dial() catch continue;\n        return;\n    }\n}"
          }
        ]
      }
    },
    {
      "id": "rule-16-3",
      "number": "16.3",
      "kind": "rule",
      "group": "Control flow",
      "obligation": "Prongs do not fall through. The compiler has no fallthrough. Two tags that share a body are one prong, joined with a comma. Do not paste the next case's body into this prong to fake the fall.",
      "do": {
        "zig": [
          "const Color = enum { red, blue };\n\nfn nameOf(c: Color) []const u8 {\n    return switch (c) {\n        .red => \"red\",\n        .blue => \"blue\",\n    };\n}",
          "fn warm(c: Color) bool {\n    return switch (c) {\n        .red, .blue => true,\n    };\n}"
        ],
        "note": "Shared body:"
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": ".red adds 1 and then also adds 2 because someone meant to fall into .blue. .blue alone adds 2. .red now means something else, and a new tag will not inherit either body.",
            "zig": "fn score(c: Color) u8 {\n    var total: u8 = 0;\n    switch (c) {\n        .red => {\n            total += 1;\n            total += 2;\n        },\n        .blue => total += 2,\n    }\n    return total;\n}"
          }
        ]
      }
    },
    {
      "id": "rule-16-4",
      "number": "16.4",
      "kind": "rule",
      "group": "Control flow",
      "obligation": "A switch on an enum or a tagged union lists every tag. else is for an integer, or for a non-exhaustive enum, and that prong handles the unknown value. An empty else swallows a new opcode.",
      "do": {
        "zig": [
          "fn opcodeName(code: u8) error{Unknown}![]const u8 {\n    return switch (code) {\n        1 => \"load\",\n        2 => \"store\",\n        else => error.Unknown,\n    };\n}"
        ],
        "note": "nameOf has no else because Color is exhaustive. Adding a tag and forgetting a prong is rejected."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "Code 9 is ignored.",
            "zig": "fn ignore(code: u8) void {\n    switch (code) {\n        1 => {},\n        else => {},\n    }\n}"
          },
          {
            "mark": "rejected",
            "why": "An uncalled function is not analyzed, so the sample is exported. The tag is u8 so the diagnostic is the missing prong.",
            "zig": "const E = enum(u8) { a, b };\nexport fn missingProng(e: E) void {\n    switch (e) {\n        .a => {},\n    }\n}"
          }
        ]
      }
    },
    {
      "id": "rule-17-1",
      "number": "17.1",
      "kind": "rule",
      "group": "Functions and linkage",
      "obligation": "A variadic function is not defined in Zig: no ... parameter read with @cVaStart, @cVaArg, and @cVaEnd. Nothing checks how many arguments the caller passed or what types they have, in any build mode. Take a slice.",
      "do": {
        "zig": [
          "fn sumAll(values: []const i32) error{Overflow}!i32 {\n    var sum: i32 = 0;\n    for (values) |v| sum = try std.math.add(i32, sum, v);\n    return sum;\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "A caller that passes an int gets garbage. In a 0.17.0 run, firstValue(1, @as(c_int, 7)) returned 0 in Debug and in ReleaseSafe, with no check.",
            "zig": "export fn firstValue(count: c_int, ...) callconv(.c) f64 {\n    _ = count;\n    var ap = @cVaStart();\n    defer @cVaEnd(&ap);\n    return @cVaArg(&ap, f64);\n}"
          }
        ]
      }
    },
    {
      "id": "rule-17-2",
      "number": "17.2",
      "kind": "rule",
      "group": "Functions and linkage",
      "obligation": "No runtime recursion, direct or through another function. A tree is a loop, an explicit stack with a fixed capacity, and a separate bound on how many nodes the loop visits. A cycle does not grow the stack, so the visit bound is what stops it. Recursion that exists only at comptime, to build a type, is a different mechanism. It is not a recursive algorithm, and @setEvalBranchQuota still bounds the compiler.",
      "do": {
        "zig": [
          "const Node = struct {\n    value: u32,\n    left: ?*const Node,\n    right: ?*const Node,\n};\n\nfn sumTree(root: *const Node) error{ Overflow, TooDeep }!u32 {\n    var total: u32 = 0;\n    var stack: [64]*const Node = undefined;\n    var len: usize = 1;\n    var seen: usize = 0;\n    stack[0] = root;\n    while (len > 0) {\n        if (seen == stack.len) return error.TooDeep;\n        seen += 1;\n        len -= 1;\n        const node = stack[len];\n        total = try std.math.add(u32, total, node.value);\n        if (node.left) |left| {\n            if (len == stack.len) return error.TooDeep;\n            stack[len] = left;\n            len += 1;\n        }\n        if (node.right) |right| {\n            if (len == stack.len) return error.TooDeep;\n            stack[len] = right;\n            len += 1;\n        }\n    }\n    return total;\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "A deep chain overflows the call stack. Zig 0.17 does not insert a stack check on this call.",
            "zig": "fn walk(node: *const Node) void {\n    if (node.left) |left| walk(left);\n    if (node.right) |right| walk(right);\n}"
          }
        ]
      }
    },
    {
      "id": "rule-18-4",
      "number": "18.4",
      "kind": "rule",
      "group": "Initialization, pointers, and unions",
      "obligation": "+ and - do not take a many-item or C pointer ([*]T, [*c]T). A many-item pointer has no length, so nothing checks where the result points, in any build mode. The function that learns the length slices the pointer once, checks the length, and indexes the slice. < and > between two pointers do not compile.",
      "do": {
        "zig": [
          "fn third(p: [*]const u32, len: usize) error{OutOfRange}!u32 {\n    const items = p[0..len];\n    if (items.len < 3) return error.OutOfRange;\n    return items[2];\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "Nothing knows where the object ends. With an offset past the end, as in (p + 6)[0] on a four-element array, a 0.17.0 Debug run returned whatever was in memory, with no panic.",
            "zig": "fn thirdUnchecked(p: [*]const u32) u32 {\n    return (p + 2)[0];\n}"
          }
        ]
      }
    },
    {
      "id": "rule-18-6",
      "number": "18.6",
      "kind": "rule",
      "group": "Initialization, pointers, and unions",
      "obligation": "A pointer or a slice into a local does not outlive the call. Return the value, or write into storage the caller passes in.",
      "do": {
        "zig": [
          "fn fillHeader(out: *[4]u8, n: u8) []const u8 {\n    out.* = .{ n, 0, 0, 0 };\n    return out[0..];\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "local dies when makeSlice returns, and the slice points into a dead stack frame. Zig 0.17.0 inserts no check for this in any build mode.",
            "zig": "fn makeSlice(n: u8) []const u8 {\n    const local = [4]u8{ n, 0, 0, 0 };\n    return local[0..];\n}"
          }
        ]
      }
    },
    {
      "id": "rule-19-1",
      "number": "19.1",
      "kind": "rule",
      "group": "Buffers and strings",
      "obligation": "@memcpy does not copy between two regions that overlap. Moving data inside one buffer is @memmove. A safe build checks @memcpy for overlap and panics. ReleaseFast does not check, and the language reference requires the two regions not to overlap.",
      "do": {
        "zig": [
          "fn dropFront(buf: []u8, n: usize) error{OutOfRange}![]u8 {\n    if (n > buf.len) return error.OutOfRange;\n    const rest = buf.len - n;\n    @memmove(buf[0..rest], buf[n..]);\n    return buf[0..rest];\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "Source and destination share three bytes. A 0.17.0 Debug run panicked with @memcpy arguments alias.",
            "zig": "fn shiftLeft(buf: *[5]u8) void {\n    @memcpy(buf[0..4], buf[1..5]);\n}"
          }
        ]
      }
    },
    {
      "id": "rule-19-2",
      "number": "19.2",
      "kind": "rule",
      "group": "Initialization, pointers, and unions",
      "obligation": "A bare union is not used. A union that holds one of several values is union(enum), so the stored field is known and a switch reads it. extern union and packed union exist for a C header or a register layout, and stay in the file that talks to it. Reinterpreting bits on purpose is @bitCast.",
      "do": {
        "zig": [
          "const Value = union(enum) {\n    int: u32,\n    float: f32,\n};\n\nfn asFloat(v: Value) error{WrongKind}!f32 {\n    return switch (v) {\n        .float => |x| x,\n        .int => error.WrongKind,\n    };\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "The stored field is int. Reading float panics in a safe build and is illegal behavior in ReleaseFast and ReleaseSmall.",
            "zig": "const Raw = union {\n    int: u32,\n    float: f32,\n};\n\nfn wrongField(x: u32) f32 {\n    const v: Raw = .{ .int = x };\n    return v.float;\n}"
          }
        ]
      }
    },
    {
      "id": "rule-19-3",
      "number": "19.3",
      "kind": "rule",
      "group": "Initialization, pointers, and unions",
      "obligation": "A union(enum) payload is read through a switch prong capture, or after a test of the tag in the same function. Reading a field that is not the active one panics in a safe build. ReleaseFast has no check and reads the other field's bits.",
      "do": {
        "zig": [],
        "note": "asFloat reads the payload through a switch capture and returns an error for the other tag."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "floatOf(.{ .int = 7 }) panicked in a 0.17.0 Debug run with access of union field 'float' while field 'int' is active. ReleaseFast returned the bits of 7 read as an f32, about 1e-44.",
            "zig": "const Value = union(enum) {\n    int: u32,\n    float: f32,\n};\n\nfn floatOf(v: Value) f32 {\n    return v.float;\n}"
          }
        ]
      }
    },
    {
      "id": "rule-21-3",
      "number": "21.3",
      "kind": "rule",
      "group": "Memory and other resources",
      "obligation": "malloc, free, and std.heap.c_allocator are not the allocator. Take std.mem.Allocator. try is the null check: alloc returns error.OutOfMemory.",
      "do": {
        "zig": [
          "fn grow(allocator: std.mem.Allocator) ![]u32 {\n    var list: std.ArrayList(u32) = .empty;\n    errdefer list.deinit(allocator);\n    try list.append(allocator, 1);\n    return try list.toOwnedSlice(allocator);\n}"
        ],
        "note": "take and grow.\n\nstd.ArrayList in 0.17 does not store the allocator. Pass the same one to every method and to deinit."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "when libc is linked. c_allocator.alloc is malloc with a Zig type. The matching c_allocator.free is free. Library code that calls either one cannot be checked by std.testing.allocator. Without -lc the compiler rejects c_allocator.",
            "zig": "fn buffer(n: usize) ![]u8 {\n    return std.heap.c_allocator.alloc(u8, n);\n}"
          }
        ]
      }
    },
    {
      "id": "rule-21-8",
      "number": "21.8",
      "kind": "rule",
      "group": "Functions and linkage",
      "obligation": "Only main ends the process. A library does not call @panic, std.debug.panic, or std.process.exit: bad input from outside is a returned error, and the caller decides. A test may panic.",
      "do": {
        "zig": [
          "const Op = enum { nop, halt };\n\nfn decodeOp(byte: u8) error{BadOpcode}!Op {\n    return switch (byte) {\n        0 => .nop,\n        1 => .halt,\n        else => error.BadOpcode,\n    };\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "One bad byte from a file or a peer ends the whole process.",
            "zig": "const Op = enum { nop, halt };\n\nfn decodeOrPanic(byte: u8) Op {\n    return switch (byte) {\n        0 => .nop,\n        1 => .halt,\n        else => @panic(\"bad opcode\"),\n    };\n}"
          }
        ]
      }
    },
    {
      "id": "rule-21-17",
      "number": "21.17",
      "kind": "rule",
      "group": "Buffers and strings",
      "obligation": "A copy or a format stays inside the destination object. Zig text is []const u8 (pointer and length). A C string is [*:0]const u8, and std.mem.span runs only when a zero sits inside that object.",
      "do": {
        "zig": [
          "fn fromC(p: [*:0]const u8) []const u8 {\n    return std.mem.span(p);\n}\n\nfn render(buf: []u8, n: u32) error{NoSpaceLeft}![]u8 {\n    return std.mem.print(buf, \"{d}\", .{n});\n}\n\nfn copyBounded(dest: []u8, src: []const u8) error{NoSpaceLeft}!void {\n    if (dest.len < src.len) return error.NoSpaceLeft;\n    @memmove(dest[0..src.len], src);\n}"
        ],
        "note": "std.mem.print returns error.NoSpaceLeft instead of writing past buf. The returned slice points at buf, so buf has to outlive it. std.fmt.bufPrint is deprecated. @memcpy requires equal lengths and no overlap. A helper that takes two slices from its caller cannot know whether they overlap, so copyBounded uses @memmove."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "span walks until a zero. If raw is a length-delimited buffer with no terminator inside it, the walk leaves the object. Safe builds do not know where the object ends.",
            "zig": "fn asText(raw: [*]const u8) []const u8 {\n    return std.mem.span(@as([*:0]const u8, @ptrCast(raw)));\n}"
          },
          {
            "mark": "compiles",
            "why": "The index buf.len is one past the slice. Safe builds panic. ReleaseFast is illegal behavior.\n\nUse std.mem.printSentinel when the result has to be [:0]u8. The sentinel is inside the buffer you passed.",
            "zig": "fn terminate(buf: []u8) void {\n    buf[buf.len] = 0;\n}"
          }
        ]
      }
    },
    {
      "id": "rule-21-18",
      "number": "21.18",
      "kind": "rule",
      "group": "Buffers and strings",
      "obligation": "The length passed to a copy is the length of the smaller object, and it is checked against both sides. @memcpy will accept any n you slice to. The check is yours, because ReleaseFast does not bounds-check.",
      "do": {
        "zig": [],
        "note": "copyBounded."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "as an ordinary function. n from the caller is used as both lengths. If n is larger than either slice, a safe build panics and ReleaseFast writes off the end.",
            "zig": "fn copyN(dest: []u8, src: []const u8, n: usize) void {\n    @memcpy(dest[0..n], src[0..n]);\n}"
          }
        ]
      }
    },
    {
      "id": "rule-21-25",
      "number": "21.25",
      "kind": "rule",
      "group": "Threads, locks, and atomics",
      "obligation": "Every atomic operation uses .seq_cst: the std.atomic.Value methods (load, store, swap, fetchAdd, cmpxchgStrong, and the rest) and the @atomicLoad, @atomicStore, @atomicRmw, and @cmpxchg builtins. A weaker order (.acquire, .release, .acq_rel, .monotonic, .unordered) is correct only with a proof about every other access to the same data.",
      "do": {
        "zig": [
          "fn publish(ready: *std.atomic.Value(bool)) void {\n    ready.store(true, .seq_cst);\n}\n\nfn isReady(ready: *const std.atomic.Value(bool)) bool {\n    return ready.load(.seq_cst);\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": ".monotonic orders each atomic variable on its own. A thread that sees ready true is not guaranteed to see value in slot.",
            "zig": "fn publishRelaxed(ready: *std.atomic.Value(bool), slot: *std.atomic.Value(u32), value: u32) void {\n    slot.store(value, .monotonic);\n    ready.store(true, .monotonic);\n}"
          }
        ]
      }
    },
    {
      "id": "rule-22-1",
      "number": "22.1",
      "kind": "rule",
      "group": "Memory and other resources",
      "obligation": "Every resource has one release. defer when this function always releases it. errdefer when success returns it to the caller. Do not reassign the binding the defer closes over. A lock uses the same pattern. lock returns error.Canceled without holding the mutex. The defer is registered only after lock returns, so a cancelled lock is not unlocked. Unlocking a mutex that was not taken is illegal.",
      "do": {
        "zig": [
          "fn withLock(io: std.Io, mu: *std.Io.Mutex) !void {\n    try mu.lock(io);\n    defer mu.unlock(io);\n}\n\nfn writeOwnFile(dir: std.Io.Dir, io: std.Io, name: []const u8, data: []const u8) !void {\n    const file = try dir.createFile(io, name, .{ .read = true });\n    defer file.close(io);\n    try file.writeStreamingAll(io, data);\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "The second allocation's failure returns without freeing first.\n\nThe repair is errdefer allocator.free(first); immediately after the first try.",
            "zig": "fn two(allocator: std.mem.Allocator) !void {\n    const first = try allocator.alloc(u8, 8);\n    const second = try allocator.alloc(u8, 8);\n    allocator.free(second);\n    allocator.free(first);\n}"
          }
        ]
      }
    },
    {
      "id": "rule-22-2",
      "number": "22.2",
      "kind": "rule",
      "group": "Memory and other resources",
      "obligation": "allocator.free takes the slice that alloc returned, and it takes the same allocator. Freeing a sub-slice, a stack array, or a slice from another allocator typechecks and is still wrong.",
      "do": {
        "zig": [
          "fn zeroFour(allocator: std.mem.Allocator) !void {\n    const buf = try allocator.alloc(u8, 4);\n    defer allocator.free(buf);\n    @memset(buf, 0);\n}"
        ],
        "note": "grow ends in toOwnedSlice. The caller frees that slice with the same allocator. items from the list is not a separate allocation."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "Neither call frees a block alloc returned.",
            "zig": "fn freeStack(allocator: std.mem.Allocator) void {\n    var local: [4]u8 = .{ 1, 2, 3, 4 };\n    allocator.free(&local);\n}\n\nfn freeSub(allocator: std.mem.Allocator, buf: []u8) void {\n    allocator.free(buf[1..]);\n}"
          }
        ]
      }
    },
    {
      "id": "rule-22-3",
      "number": "22.3",
      "kind": "rule",
      "group": "Memory and other resources",
      "obligation": "One open file for one job. If the function both reads and writes, open one handle that can do both (createFile with .read = true, or openFile with .mode = .read_write). Do not open the same path again on a second handle.",
      "do": {
        "zig": [],
        "note": "writeOwnFile creates one handle, writes, and closes it. readOwnFile opens one read-only handle later, after the first close."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "Two handles on name at the same time: one read-only, one write-only.",
            "zig": "fn bothEnds(dir: std.Io.Dir, io: std.Io, name: []const u8) !void {\n    const reading = try dir.openFile(io, name, .{ .mode = .read_only });\n    defer reading.close(io);\n    const writing = try dir.openFile(io, name, .{ .mode = .write_only });\n    defer writing.close(io);\n    try writing.writeStreamingAll(io, \"z\");\n}"
          }
        ]
      }
    },
    {
      "id": "rule-22-4",
      "number": "22.4",
      "kind": "rule",
      "group": "Memory and other resources",
      "obligation": "A handle opened .read_only is only read. On Linux, Zig 0.17, writeStreamingAll on that handle returns error.NotOpenForWriting. The tested sample is the last lines of docs/examples/compliant.zig.",
      "do": {
        "zig": [
          "fn readOwnFile(dir: std.Io.Dir, io: std.Io, name: []const u8, dest: []u8) !usize {\n    const file = try dir.openFile(io, name, .{ .mode = .read_only });\n    defer file.close(io);\n    return readSome(file, io, dest);\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "and the write fails at runtime. openFile opens read-only unless the options say otherwise, so the write returns error.NotOpenForWriting.",
            "zig": "fn appendNote(dir: std.Io.Dir, io: std.Io, name: []const u8) !void {\n    const file = try dir.openFile(io, name, .{});\n    defer file.close(io);\n    try file.writeStreamingAll(io, \"z\");\n}"
          }
        ]
      }
    },
    {
      "id": "rule-22-5",
      "number": "22.5",
      "kind": "rule",
      "group": "Memory and other resources",
      "obligation": "Use std.Io.File methods. file.handle is the raw descriptor. It is not a field to pass to std.posix beside the Io calls.",
      "do": {
        "zig": [],
        "note": "readSome and writeStreamingAll."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "std.posix.read takes the descriptor out of the Io implementation that owns it.",
            "zig": "fn rawRead(file: std.Io.File, dest: []u8) !usize {\n    return std.posix.read(file.handle, dest);\n}"
          }
        ]
      }
    },
    {
      "id": "rule-22-6",
      "number": "22.6",
      "kind": "rule",
      "group": "Memory and other resources",
      "obligation": "After close, the handle is not a file. The defer is the last use.",
      "do": {
        "zig": [
          "const file = try dir.openFile(io, name, .{ .mode = .read_only });\ndefer file.close(io);\nreturn readSome(file, io, dest);"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "The read is ordered after close.",
            "zig": "fn readClosed(file: std.Io.File, io: std.Io, dest: []u8) !usize {\n    file.close(io);\n    return readSome(file, io, dest);\n}"
          }
        ]
      }
    },
    {
      "id": "rule-22-7",
      "number": "22.7",
      "kind": "rule",
      "group": "Memory and other resources",
      "obligation": "End of file is error.EndOfStream from the read that just ran. It is not a data byte, and it is not a length of 0. readStreaming may return 0 without being the end of the file, so the sample does not turn the error into 0.",
      "do": {
        "zig": [
          "fn readSome(file: std.Io.File, io: std.Io, dest: []u8) !usize {\n    return file.readStreaming(io, &.{dest});\n}"
        ],
        "note": "readSome of a consumed two-byte file returns error.EndOfStream. docs/examples/compliant.zig checks that on the second read."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "0xFF in the buffer is data. Treating it as an end marker drops the rest of the file.",
            "zig": "fn untilFf(buf: []const u8) usize {\n    for (buf, 0..) |b, i| {\n        if (b == 0xFF) return i;\n    }\n    return buf.len;\n}"
          }
        ]
      }
    },
    {
      "id": "rule-22-8",
      "number": "22.8",
      "kind": "rule",
      "group": "Memory and other resources",
      "obligation": "Zig's own error unions are not errno. With -lc, std.posix is libc and std.Io can call it, so those calls can change errno too. Set errno to 0 on the line before the libc call you mean, and read it before any later call.",
      "do": {
        "zig": [
          "fn missingPath(path: [*:0]const u8) error{Failed}!c_int {\n    std.c._errno().* = 0;\n    const rc = std.c.access(path, 0);\n    return captureErrno(rc);\n}"
        ],
        "note": "captureErrno is the function under Rule 22.9. access of a missing path returns -1. Do not clear errno after it returns."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "The clear is on the wrong side of the call, so the value read afterwards is 0 even when access failed.",
            "zig": "fn tooLate(path: [*:0]const u8) c_int {\n    const rc = std.c.access(path, 0);\n    std.c._errno().* = 0;\n    if (rc == -1) return std.c._errno().*;\n    return rc;\n}"
          }
        ]
      }
    },
    {
      "id": "rule-22-9",
      "number": "22.9",
      "kind": "rule",
      "group": "Memory and other resources",
      "obligation": "Read errno only when the call reported failure, and use that value before you do anything else. The report is -1, a null pointer, or, for a call such as strtoul whose result cannot signal failure, a non-zero errno after the function cleared it to 0 just before the call. std.c.errno reads it for a return of -1. If you read it yourself, keep the same order.",
      "do": {
        "zig": [
          "fn captureErrno(rc: c_int) error{Failed}!c_int {\n    if (rc != -1) return 0;\n    const err = std.c._errno().*;\n    if (err == 0) return error.Failed;\n    return err;\n}"
        ],
        "note": ""
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "The return code said success. errno is leftover from an earlier call.",
            "zig": "fn ignoreRc() c_int {\n    return std.c._errno().*;\n}"
          }
        ]
      }
    },
    {
      "id": "rule-22-10",
      "number": "22.10",
      "kind": "rule",
      "group": "Memory and other resources",
      "obligation": "Nothing runs between a failed libc call and the read of errno. A log, an allocation, or another libc call is allowed to replace it.",
      "do": {
        "zig": [],
        "note": "captureErrno reads errno as the next operation after rc == -1. With -lc, an std.Io or std.posix call can replace errno the same way another libc call can."
      },
      "dont": {
        "cases": [
          {
            "mark": "compiles",
            "why": "std.debug.print can be a write. The errno you read afterwards can be the print's, not the failed call's.",
            "zig": "fn late(rc: c_int) c_int {\n    if (rc == -1) {\n        std.debug.print(\"failed\\n\", .{});\n        return std.c._errno().*;\n    }\n    return 0;\n}"
          }
        ]
      }
    }
  ]
}
