Files
Zigmulator/src/sometimes.zig
T

261 lines
8.3 KiB
Zig

const std = @import("std");
const Allocator = std.mem.Allocator;
const SourceLocation = std.builtin.SourceLocation;
const sometimes_section = "zigmulator_sometimes";
const site_magic: u64 = 0x5a_69_67_53_6f_6d_65_74;
pub const SiteKind = enum {
assert,
reachable,
};
pub const Site = struct {
file: []const u8,
fn_name: []const u8,
line: u32,
column: u32,
label: ?[]const u8,
kind: u8,
magic: u64,
pub fn init(comptime src: SourceLocation, comptime label: ?[]const u8, comptime kind: SiteKind) Site {
return .{
.file = src.file,
.fn_name = src.fn_name,
.line = src.line,
.column = src.column,
.label = label,
.kind = @intFromEnum(kind),
.magic = site_magic,
};
}
pub fn isValid(self: Site) bool {
return self.magic == site_magic and self.line != 0;
}
pub fn kindName(self: Site) []const u8 {
return switch (self.kind) {
@intFromEnum(SiteKind.assert) => "assert",
@intFromEnum(SiteKind.reachable) => "reachable",
else => "unknown",
};
}
fn matches(self: Site, src: SourceLocation) bool {
return self.line == src.line and
self.column == src.column and
std.mem.eql(u8, self.file, src.file);
}
fn matchesSite(self: Site, other: Site) bool {
return self.line == other.line and
self.column == other.column and
std.mem.eql(u8, self.file, other.file);
}
};
const sentinel_site = Site{
.file = "",
.fn_name = "",
.line = 0,
.column = 0,
.label = null,
.kind = @intFromEnum(SiteKind.assert),
.magic = site_magic,
};
const sentinel_site_entry linksection(sometimes_section) = sentinel_site;
extern const __start_zigmulator_sometimes: Site;
extern const __stop_zigmulator_sometimes: Site;
pub fn registerSite(comptime src: SourceLocation, comptime label: ?[]const u8, comptime kind: SiteKind) *const Site {
const SiteDecl = struct {
// TODO: maybe var instead of const?
const site linksection(sometimes_section) = Site.init(src, label, kind);
};
return &SiteDecl.site;
}
pub fn compileTimeSites() []const Site {
_ = &sentinel_site_entry;
const start: [*]const Site = @ptrCast(&__start_zigmulator_sometimes);
const stop_addr = @intFromPtr(&__stop_zigmulator_sometimes);
const start_addr = @intFromPtr(start);
const count = (stop_addr - start_addr) / @sizeOf(Site);
return start[0..count];
}
pub fn reportCompileTimeSites() void {
var count: usize = 0;
for (compileTimeSites()) |site| {
if (site.isValid()) count += 1;
}
if (count == 0) return;
std.debug.print("\n=== Sometimes assertion sites: {d} compiled ===\n", .{count});
for (compileTimeSites()) |site| {
if (!site.isValid()) continue;
std.debug.print(" [{s}] {s}:{d}:{d} ({s})", .{
site.kindName(), site.file, site.line, site.column, site.fn_name,
});
if (site.label) |label|
std.debug.print(" \"{s}\"", .{label});
std.debug.print("\n", .{});
}
std.debug.print("\n", .{});
}
// Registry of "sometimes" assertions.
//
// A sometimes-assertion records that a given condition was observed to be true
// at least once during a simulation. Unlike a regular assertion (which must
// always hold), a sometimes-assertion documents a branch we *expect* to be
// exercised eventually. In a single run it may legitimately never fire; its
// signal is only meaningful as coverage across many seeds.
//
// Each call site is identified by its source location (@src()), so the same
// physical assertSometimes() call is one entry regardless of how many times
// it is evaluated. An optional human-readable label is shown in the report.
//
// The registry is seeded with compile-time call-site descriptors, so the final
// report can include branches that were compiled but never evaluated.
pub const Sometimes = struct {
const Entry = struct {
file: []const u8,
fn_name: []const u8,
line: u32,
column: u32,
label: ?[]const u8,
kind_name: []const u8,
eval_count: u64,
true_count: u64,
fn matchesSite(self: Entry, site: Site) bool {
return self.line == site.line and
self.column == site.column and
std.mem.eql(u8, self.file, site.file);
}
};
gpa: Allocator,
entries: std.ArrayList(Entry),
reported: bool,
pub fn init(self: *Sometimes, gpa: Allocator) void {
self.gpa = gpa;
self.entries = .empty;
self.reported = false;
}
pub fn seedCompileTimeSites(self: *Sometimes) void {
for (compileTimeSites()) |site| {
if (!site.isValid()) continue;
self.ensureSite(site);
}
}
pub fn deinit(self: *Sometimes) void {
self.entries.deinit(self.gpa);
}
// Records one evaluation of a sometimes-assertion. The strings carried by
// @src() (and any string-literal label) live for the whole program, so we
// can store the slices directly without duplicating them.
pub fn record(self: *Sometimes, cond: bool, site: Site) void {
for (self.entries.items) |*entry| {
if (entry.matchesSite(site)) {
entry.eval_count += 1;
if (cond) entry.true_count += 1;
if (site.label != null and entry.label == null) entry.label = site.label;
return;
}
}
// First time this call site is seen. If we can't grow the registry we
// simply drop the entry: a sometimes-assertion must never affect the
// behaviour of the program under test.
self.appendSite(site, 1, if (cond) 1 else 0);
}
fn ensureSite(self: *Sometimes, site: Site) void {
for (self.entries.items) |entry| {
if (site.matchesSite(.{
.file = entry.file,
.fn_name = entry.fn_name,
.line = entry.line,
.column = entry.column,
.label = entry.label,
.kind = 0,
.magic = site_magic,
})) return;
}
self.appendSite(site, 0, 0);
}
fn appendSite(self: *Sometimes, site: Site, eval_count: u64, true_count: u64) void {
self.entries.append(self.gpa, .{
.file = site.file,
.fn_name = site.fn_name,
.line = site.line,
.column = site.column,
.label = site.label,
.kind_name = site.kindName(),
.eval_count = eval_count,
.true_count = true_count,
}) catch return;
}
pub fn allReached(self: Sometimes) bool {
for (self.entries.items) |entry| {
if (entry.true_count == 0) return false;
}
return true;
}
pub fn report(self: *Sometimes) void {
self.reported = true;
if (self.entries.items.len == 0)
return;
var taken: usize = 0;
for (self.entries.items) |entry| {
if (entry.true_count > 0) taken += 1;
}
std.debug.print("\n=== Sometimes assertions: {d}/{d} taken ===\n", .{ taken, self.entries.items.len });
for (self.entries.items) |entry| {
// A reachable site has no meaningful condition, so its true/eval
// ratio is always N/N; print just the hit count. An assert site's
// ratio shows how often the condition held, which is useful.
if (std.mem.eql(u8, entry.kind_name, "reachable")) {
std.debug.print(" [{d}] {s}:{d}:{d} ({s})", .{
entry.true_count,
entry.file,
entry.line,
entry.column,
entry.fn_name,
});
} else {
std.debug.print(" [{d}/{d}] {s}:{d}:{d} ({s})", .{
entry.true_count,
entry.eval_count,
entry.file,
entry.line,
entry.column,
entry.fn_name,
});
}
if (entry.label) |label|
std.debug.print(" \"{s}\"", .{label});
std.debug.print("\n", .{});
}
std.debug.print("\n", .{});
}
};