Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/cache/file_cache.zig
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ pub const FileCache = struct {
return z.fixedArrayLen(node) catch null;
}

/// Comptime-known non-negative value of `node`, or null when not
/// comptime-resolvable / engine unavailable. A comptime index can't cause a
/// runtime underflow/OOB (it would be a compile error), so a non-null result
/// means the access is comptime-verified safe.
pub fn comptimeIntValueOf(self: *FileCache, node: std.zig.Ast.Node.Index) ?u64 {
const z = self.zls orelse return null;
return z.comptimeIntValue(node) catch null;
}

/// For a type-name reference node `T`: whether `T` denotes a pointer/
/// optional type (true), a value type (false), or couldn't resolve (null).
/// Null when the type engine is unavailable.
Expand All @@ -98,6 +107,14 @@ pub const FileCache = struct {
return z.intInfo(node) catch null;
}

/// Whether `node`'s type resolves to a floating-point type. False when the
/// type engine is unavailable or the type isn't a float. Floats are not
/// containers, so `typeNameOfNode` can't report them — use this instead.
pub fn isFloatType(self: *FileCache, node: std.zig.Ast.Node.Index) bool {
const z = self.zls orelse return false;
return z.isFloatType(node) catch false;
}

/// For `x.ptr` (x a slice): whether x's element is byte-sized (align 1) —
/// true=byte slice, false=wider element, null=unresolved/unknown. Null
/// when the type engine is unavailable.
Expand Down
418 changes: 411 additions & 7 deletions src/flow/value_range.zig

Large diffs are not rendered by default.

191 changes: 191 additions & 0 deletions src/flow/value_range_tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,126 @@ test "nonzero: guard in while condition refines body" {
, "i", "buf"));
}

test "nonzero: use on LHS of a subscript write (buf[i-1] = …)" {
// Regression: analyzeAssign only inspected the RHS for the use token, so a
// provably-nonzero index on the WRITE side (`buf[i-1] = 0`) was never
// resolved and the rule false-positived. `.len` guard needs no type engine.
try std.testing.expect(try proveAt(
\\fn f(i: usize, buf: []u8, other: []const u8) void {
\\ if (i > other.len) {
\\ buf[i - 1] = 0;
\\ }
\\}
\\
, "i", "buf"));
}

test "nonzero: LHS subscript write under a bare (no-block) if" {
try std.testing.expect(try proveAt(
\\fn f(i: usize, buf: []u8, other: []const u8) void {
\\ if (i > other.len) buf[i - 1] = 0;
\\}
\\
, "i", "buf"));
}

test "nonzero: no false-negative — unguarded LHS write still unproven" {
try std.testing.expect(!try proveAt(
\\fn f(i: usize, buf: []u8) void {
\\ buf[i - 1] = 0;
\\}
\\
, "i", "buf"));
}

test "nonzero: cross-fn ceil-div postcondition (numMasks) with guarded arg" {
// `m = numMasks(k)` where numMasks is `(x + (D-1)) / D` (ceil-div, structural
// D-1 == D-1), and k is guarded non-zero ⟹ m non-zero. Caller listed first so
// `proveAt` analyzes it; the callee scan finds `numMasks` regardless of order.
try std.testing.expect(try proveAt(
\\fn f(a: []u8, k: usize) u8 {
\\ if (k == 0) return 0;
\\ const m = numMasks(k);
\\ return a[m - 1];
\\}
\\fn numMasks(x: usize) usize {
\\ return (x + (64 - 1)) / 64;
\\}
, "m", "a"));
}

test "nonzero: cross-fn — unguarded arg leaves result UNPROVEN (no false-negative)" {
try std.testing.expect(!try proveAt(
\\fn f(a: []u8, k: usize) u8 {
\\ const m = numMasks(k);
\\ return a[m - 1];
\\}
\\fn numMasks(x: usize) usize {
\\ return (x + (64 - 1)) / 64;
\\}
, "m", "a"));
}

test "nonzero: cross-fn via field-path value-alias (const L = s.len guards s.len arg)" {
try std.testing.expect(try proveAt(
\\fn f(s: S, a: []u8) u8 {
\\ const L = s.len;
\\ if (L == 0) return 0;
\\ const m = numMasks(s.len);
\\ return a[m - 1];
\\}
\\fn numMasks(x: usize) usize {
\\ return (x + (64 - 1)) / 64;
\\}
, "m", "a"));
}

test "nonempty: `s = base[0..n]` with n guarded nonzero ⟹ s non-empty" {
try std.testing.expect(try proveNonemptyAt(
\\fn f(buf: []u8, n: usize) u8 {
\\ if (n == 0) return 0;
\\ const s = buf[0..n];
\\ return s[s.len - 1];
\\}
, "s", "s"));
}

test "nonempty: `s = base[0..n]` with n unguarded is UNPROVEN (no false-negative)" {
try std.testing.expect(!try proveNonemptyAt(
\\fn f(buf: []u8, n: usize) u8 {
\\ const s = buf[0..n];
\\ return s[s.len - 1];
\\}
, "s", "s"));
}

test "nonempty: short-circuit `c.len > 0 and c[c.len-1]` (endsWith idiom)" {
try std.testing.expect(try proveNonemptyAt(
\\fn endsWith(self: []const u8, ch: u8) bool {
\\ return self.len > 0 and self[self.len - 1] == ch;
\\}
\\
, "self", "self"));
}

test "nonempty: short-circuit `c.len == 0 or c[c.len-1]` (or-form)" {
try std.testing.expect(try proveNonemptyAt(
\\fn endsWithOrEmpty(self: []const u8, ch: u8) bool {
\\ return self.len == 0 or self[self.len - 1] == ch;
\\}
\\
, "self", "self"));
}

test "nonempty: no false-negative — unguarded access in same fn still unproven" {
try std.testing.expect(!try proveNonemptyAt(
\\fn last(self: []const u8) u8 {
\\ return self[self.len - 1];
\\}
\\
, "self", "self"));
}

test "nonzero: and-chain guard (i > 0 and c)" {
try std.testing.expect(try proveAt(
\\fn f(i: usize, c: bool, buf: []const u8) u8 {
Expand Down Expand Up @@ -246,6 +366,52 @@ fn proveNonemptyAt(src: [:0]const u8, container: []const u8, arr: []const u8) !b
return value_range.provesNonempty(gpa, &tree, body, container, lbracket, null);
}

/// Query `provesMinLen(container, n)` at the `[` token after identifier `arr`.
fn proveMinLenAt(src: [:0]const u8, container: []const u8, n: u64, arr: []const u8) !bool {
const gpa = std.testing.allocator;
var tree = try Ast.parse(gpa, src, .zig);
defer tree.deinit(gpa);
const body = firstFnBody(&tree) orelse return error.NoFnBody;
const lbracket = subscriptLBracket(&tree, arr) orelse return error.NoSubscript;
return value_range.provesMinLen(gpa, &tree, body, container, n, lbracket, null);
}

test "minlen: switch-arm bounds a slice-from-range length (k.len >= 16 in arm 20)" {
try std.testing.expect(try proveMinLenAt(
\\fn f(b: []const u8) u8 {
\\ const n = b.len;
\\ const k = b[0..n];
\\ return switch (n) {
\\ 20 => k[16],
\\ else => 0,
\\ };
\\}
, "k", 16, "k"));
}

test "minlen: switch arm BELOW threshold is UNPROVEN (no false-negative)" {
try std.testing.expect(!try proveMinLenAt(
\\fn f(b: []const u8) u8 {
\\ const n = b.len;
\\ const k = b[0..n];
\\ return switch (n) {
\\ 10 => k[16],
\\ else => 0,
\\ };
\\}
, "k", 16, "k"));
}

test "minlen: no switch ⟹ unproven (no false-negative)" {
try std.testing.expect(!try proveMinLenAt(
\\fn f(b: []const u8) u8 {
\\ const n = b.len;
\\ const k = b[0..n];
\\ return k[16];
\\}
, "k", 16, "k"));
}

test "nonempty: bare arr.len-1 is unknown (fires)" {
try std.testing.expect(!try proveNonemptyAt(
\\fn f(arr: []const u8) u8 {
Expand Down Expand Up @@ -518,3 +684,28 @@ test "diff-alias: rebinding end clears diff fact" {
\\
, "end_mut", "buf"));
}

test "nonzero: loop var only `+=`'d keeps its positive init (monotonic)" {
try std.testing.expect(try proveAt(
\\fn f(buf: []u8) void {
\\ var p: usize = 1;
\\ while (p < buf.len) {
\\ buf[p - 1] = 0;
\\ p += 1;
\\ }
\\}
, "p", "buf"));
}

test "nonzero: loop var reset to 0 then accessed is UNPROVEN (no false-negative)" {
try std.testing.expect(!try proveAt(
\\fn f(buf: []u8) void {
\\ var p: usize = 1;
\\ while (p < buf.len) {
\\ p = 0;
\\ buf[p - 1] = 0;
\\ p += 1;
\\ }
\\}
, "p", "buf"));
}
1 change: 1 addition & 0 deletions src/lib.zig
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub const Problem = problem_mod.Problem;
pub const Note = problem_mod.Note;
pub const Pos = problem_mod.Pos;
pub const Severity = problem_mod.Severity;
pub const Verdict = problem_mod.Verdict;
pub const Rule = rule_catalog_mod.Rule;
pub const rule_catalog = rule_catalog_mod.all;
pub const lookupRule = rule_catalog_mod.lookup;
Expand Down
Loading
Loading