From 53aa5091532e9b1e9e22caa75f6d4c3755b47a9e Mon Sep 17 00:00:00 2001 From: cozis Date: Fri, 19 Jun 2026 13:01:04 +0200 Subject: [PATCH] simulator: expose sometimes assertions via the simulator object --- src/simulator.zig | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/simulator.zig b/src/simulator.zig index d5c620f..030c1ce 100644 --- a/src/simulator.zig +++ b/src/simulator.zig @@ -7,9 +7,30 @@ const Trace = @import("trace.zig").Trace; const Scheduler = @import("scheduler.zig"); const Network = @import("network.zig"); const Node = @import("node.zig"); +const Sometimes = @import("sometimes.zig").Sometimes; pub const EntryPoint = Scheduler.MainEntryPoint; +// Process-global pointer to the active simulation's sometimes-assertion +// registry. Programs under test only receive an `std.Io` and have no handle to +// the Simulator, so assertSometimes() reaches the registry through this global. +// Everything runs single-threaded in userspace, so a plain global is +// deterministic. It is set in init() and cleared in deinit(), giving each +// simulation a fresh, per-run registry. +var g_sometimes: ?*Sometimes = null; + +// Records that `cond` was observed at this point of the simulation. The call +// site (@src()) is the identity; `label` is an optional human-readable name +// shown in the end-of-simulation report. Pass null when no label is wanted: +// assertSometimes(x > 0, @src(), null); +// +// This never changes the behaviour of the program under test: if no simulation +// is active the call is a no-op. +pub fn assertSometimes(cond: bool, src: std.builtin.SourceLocation, label: ?[]const u8) void { + const registry = g_sometimes orelse return; + registry.record(cond, src, label); +} + const SpawnOptions = struct { stack_size: usize = 64 * 1024, addresses: []const u32 = &[0]u32 {}, // TODO: How do I make an empty slice? @@ -34,6 +55,7 @@ nodes: std.ArrayList(*Node), executables: std.ArrayList(ExecutableName), real_io: std.Io, next_node_id: u32, +sometimes: Sometimes, pub fn init(self: *Simulator, gpa: Allocator, real_io: std.Io, seed: u64) void { self.gpa = gpa; @@ -45,9 +67,18 @@ pub fn init(self: *Simulator, gpa: Allocator, real_io: std.Io, seed: u64) void { self.executables = .empty; self.real_io = real_io; self.next_node_id = 0; + self.sometimes.init(gpa); + g_sometimes = &self.sometimes; } pub fn deinit(self: *Simulator) void { + // Print the end-of-simulation coverage report unless the caller already + // asked for it explicitly via reportSometimes(). + if (!self.sometimes.reported) + self.sometimes.report(); + g_sometimes = null; + self.sometimes.deinit(); + for (self.nodes.items) |node| { node.deinit(); self.gpa.destroy(node); @@ -59,6 +90,13 @@ pub fn deinit(self: *Simulator) void { self.trace.deinit(); } +// Prints which sometimes-assertions were taken (✓) and which were reached +// but never satisfied (✗). Called automatically by deinit(); expose it so +// callers can choose exactly when the report is emitted. +pub fn reportSometimes(self: *Simulator) void { + self.sometimes.report(); +} + pub fn setTraceOutputFile(self: *Simulator, path: []const u8) !void { try self.trace.setOutputFile(path); }