{
  "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-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-5-1",
      "number": "5.1",
      "kind": "dir",
      "group": "Concurrency",
      "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-3",
      "number": "5.3",
      "kind": "dir",
      "group": "Concurrency",
      "obligation": "Threads are created at start-up, a fixed number of them, and every one is joined. No thread is spawned per request or inside a while loop, and none is detached.",
      "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-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-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-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-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-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-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-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}"
          }
        ]
      }
    }
  ]
}
