lib/choir/src/passes/reproducer.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const alloc_arena = @import("alloc_arena");
  3 const sys = @import("sys");
  4 
  5 const ir = @import("../core/root.zig");
  6 const dialects = @import("../dialects/root.zig");
  7 const pass_mod = @import("pass/root.zig");
  8 const pipeline_mod = @import("pipeline.zig");
  9 const textual = @import("textual.zig");
 10 
 11 const magic = "choir-pass-reproducer-v1";
 12 
 13 pub const ReplayOptions = struct {
 14     worker_allocator: ?std.mem.Allocator = null,
 15 };
 16 
 17 pub const ReplayResult = struct {
 18     result: pass_mod.PassResult,
 19     stats: pass_mod.PassManagerStats,
 20     failure_kind: ?pass_mod.PassFailureKind,
 21     worker_count: usize,
 22 };
 23 
 24 pub fn formatPassFailureReproducerAlloc(
 25     allocator: std.mem.Allocator,
 26     reproducer: *const pass_mod.PassFailureReproducer,
 27 ) ![]u8 {
 28     var out = std.Io.Writer.Allocating.init(allocator);
 29     defer out.deinit();
 30     try writePassFailureReproducer(&out.writer, reproducer);
 31     return try out.toOwnedSlice();
 32 }
 33 
 34 pub fn writePassFailureReproducer(
 35     writer: *std.Io.Writer,
 36     reproducer: *const pass_mod.PassFailureReproducer,
 37 ) !void {
 38     try writer.writeAll(magic ++ "\n");
 39     try writer.print("max_threads {d}\n", .{reproducer.max_threads});
 40     try writer.print("worker_count {d}\n", .{reproducer.worker_count});
 41     try writer.print("verifier_enabled {s}\n", .{if (reproducer.verifier_enabled) "true" else "false"});
 42     try writer.print("failure_kind {s}\n", .{failureKindName(reproducer.failure_kind)});
 43     try writeOptionalBytes(writer, "pass_name", reproducer.pass_name);
 44     try writeOptionalBytes(writer, "target_op_name", reproducer.target_op_name);
 45     try writeOptionalBytes(writer, "target_symbol_name", reproducer.target_symbol_name);
 46     try writeOptionalBytes(writer, "verifier_error", verifierErrorName(reproducer));
 47     try writeRequiredBytes(writer, "pipeline", reproducer.pipeline);
 48     try writeRequiredBytes(writer, "ir", reproducer.ir);
 49 }
 50 
 51 pub fn writePassFailureReproducerFile(
 52     allocator: std.mem.Allocator,
 53     path: []const u8,
 54     reproducer: *const pass_mod.PassFailureReproducer,
 55 ) !void {
 56     const bytes = try formatPassFailureReproducerAlloc(allocator, reproducer);
 57     defer allocator.free(bytes);
 58     try sys.fs.writeFile(path, bytes);
 59 }
 60 
 61 pub fn parsePassFailureReproducerAlloc(
 62     allocator: std.mem.Allocator,
 63     text: []const u8,
 64 ) !pass_mod.PassFailureReproducer {
 65     var parser = FileParser{
 66         .allocator = allocator,
 67         .text = text,
 68     };
 69     return try parser.parse();
 70 }
 71 
 72 pub fn replayPassFailureReproducer(
 73     allocator: std.mem.Allocator,
 74     registry: *const pipeline_mod.PassRegistry,
 75     ctx: *ir.Context,
 76     reproducer: *const pass_mod.PassFailureReproducer,
 77     options: ReplayOptions,
 78 ) !ReplayResult {
 79     if (reproducer.failure_kind == .exhausted) return error.UnrecordedWorkBudget;
 80 
 81     var manager = pass_mod.PassManager.init(allocator);
 82     defer manager.deinit();
 83 
 84     try textual.parsePassPipeline(registry, reproducer.pipeline, &manager);
 85     if (reproducer.verifier_enabled) manager.enableVerifier();
 86 
 87     const op = try ir.parse.operation(ctx, reproducer.ir);
 88     defer op.erase();
 89 
 90     const result = manager.runWithOptions(op, ctx, .{
 91         .max_threads = reproducer.max_threads,
 92         .worker_allocator = options.worker_allocator orelse allocator,
 93     });
 94 
 95     const replayed = manager.getLastFailureReproducer();
 96     return .{
 97         .result = result,
 98         .stats = manager.stats,
 99         .failure_kind = if (replayed) |failure| failure.failure_kind else null,
100         .worker_count = if (replayed) |failure| failure.worker_count else 1,
101     };
102 }
103 
104 fn writeOptionalBytes(writer: *std.Io.Writer, name: []const u8, value: ?[]const u8) !void {
105     if (value) |bytes| {
106         try writeRequiredBytes(writer, name, bytes);
107     } else {
108         try writer.print("{s} none\n", .{name});
109     }
110 }
111 
112 fn writeRequiredBytes(writer: *std.Io.Writer, name: []const u8, bytes: []const u8) !void {
113     try writer.print("{s} bytes {d}\n", .{ name, bytes.len });
114     try writer.writeAll(bytes);
115     try writer.writeByte('\n');
116 }
117 
118 fn failureKindName(kind: ?pass_mod.PassFailureKind) []const u8 {
119     return switch (kind orelse return "none") {
120         .exhausted => "exhausted",
121         .pass => "pass",
122         .verifier => "verifier",
123         .target => "target",
124     };
125 }
126 
127 fn parseFailureKind(text: []const u8) !?pass_mod.PassFailureKind {
128     if (std.mem.eql(u8, text, "none")) return null;
129     if (std.mem.eql(u8, text, "exhausted")) return .exhausted;
130     if (std.mem.eql(u8, text, "pass")) return .pass;
131     if (std.mem.eql(u8, text, "verifier")) return .verifier;
132     if (std.mem.eql(u8, text, "target")) return .target;
133     return error.InvalidPassFailureReproducer;
134 }
135 
136 fn verifierErrorName(reproducer: *const pass_mod.PassFailureReproducer) ?[]const u8 {
137     if (reproducer.verifier_error_name) |name| return name;
138     if (reproducer.verifier_error) |err| return @errorName(err);
139     return null;
140 }
141 
142 const FileParser = struct {
143     allocator: std.mem.Allocator,
144     text: []const u8,
145     index: usize = 0,
146 
147     fn parse(self: *FileParser) !pass_mod.PassFailureReproducer {
148         try self.expectLine(magic);
149         const max_threads = try self.readUsizeField("max_threads");
150         const worker_count = try self.readUsizeField("worker_count");
151         const verifier_enabled = try self.readBoolField("verifier_enabled");
152         const failure_kind = try parseFailureKind(try self.readScalarField("failure_kind"));
153         const pass_name = try self.readOptionalBytes("pass_name");
154         errdefer if (pass_name) |value| self.allocator.free(value);
155         const target_op_name = try self.readOptionalBytes("target_op_name");
156         errdefer if (target_op_name) |value| self.allocator.free(value);
157         const target_symbol_name = try self.readOptionalBytes("target_symbol_name");
158         errdefer if (target_symbol_name) |value| self.allocator.free(value);
159         const verifier_error_name = try self.readOptionalBytes("verifier_error");
160         errdefer if (verifier_error_name) |value| self.allocator.free(value);
161         const pipeline = try self.readRequiredBytes("pipeline");
162         errdefer self.allocator.free(pipeline);
163         const snapshot = try self.readRequiredBytes("ir");
164         errdefer self.allocator.free(snapshot);
165         if (self.index != self.text.len) return error.InvalidPassFailureReproducer;
166 
167         return .{
168             .pipeline = pipeline,
169             .ir = snapshot,
170             .max_threads = max_threads,
171             .worker_count = worker_count,
172             .verifier_enabled = verifier_enabled,
173             .failure_kind = failure_kind,
174             .pass_name = pass_name,
175             .target_op_name = target_op_name,
176             .target_symbol_name = target_symbol_name,
177             .verifier_error = null,
178             .verifier_error_name = verifier_error_name,
179         };
180     }
181 
182     fn expectLine(self: *FileParser, expected: []const u8) !void {
183         const line = try self.readLine();
184         if (!std.mem.eql(u8, line, expected)) return error.InvalidPassFailureReproducer;
185     }
186 
187     fn readUsizeField(self: *FileParser, name: []const u8) !usize {
188         const value = try self.readScalarField(name);
189         return std.fmt.parseUnsigned(usize, value, 10) catch error.InvalidPassFailureReproducer;
190     }
191 
192     fn readBoolField(self: *FileParser, name: []const u8) !bool {
193         const value = try self.readScalarField(name);
194         if (std.mem.eql(u8, value, "true")) return true;
195         if (std.mem.eql(u8, value, "false")) return false;
196         return error.InvalidPassFailureReproducer;
197     }
198 
199     fn readScalarField(self: *FileParser, name: []const u8) ![]const u8 {
200         const line = try self.readLine();
201         if (!std.mem.startsWith(u8, line, name)) return error.InvalidPassFailureReproducer;
202         if (line.len <= name.len or line[name.len] != ' ') return error.InvalidPassFailureReproducer;
203         return line[name.len + 1 ..];
204     }
205 
206     fn readOptionalBytes(self: *FileParser, name: []const u8) !?[]u8 {
207         const line = try self.readLine();
208         if (!std.mem.startsWith(u8, line, name)) return error.InvalidPassFailureReproducer;
209         if (line.len <= name.len or line[name.len] != ' ') return error.InvalidPassFailureReproducer;
210         const rest = line[name.len + 1 ..];
211         if (std.mem.eql(u8, rest, "none")) return null;
212         return try self.readBytesAfterHeader(rest);
213     }
214 
215     fn readRequiredBytes(self: *FileParser, name: []const u8) ![]u8 {
216         const line = try self.readLine();
217         if (!std.mem.startsWith(u8, line, name)) return error.InvalidPassFailureReproducer;
218         if (line.len <= name.len or line[name.len] != ' ') return error.InvalidPassFailureReproducer;
219         return try self.readBytesAfterHeader(line[name.len + 1 ..]);
220     }
221 
222     fn readBytesAfterHeader(self: *FileParser, rest: []const u8) ![]u8 {
223         if (!std.mem.startsWith(u8, rest, "bytes ")) return error.InvalidPassFailureReproducer;
224         const len = std.fmt.parseUnsigned(usize, rest["bytes ".len..], 10) catch return error.InvalidPassFailureReproducer;
225         if (self.index + len > self.text.len) return error.InvalidPassFailureReproducer;
226         const bytes = self.text[self.index .. self.index + len];
227         self.index += len;
228         if (self.index >= self.text.len or self.text[self.index] != '\n') return error.InvalidPassFailureReproducer;
229         self.index += 1;
230         return try self.allocator.dupe(u8, bytes);
231     }
232 
233     fn readLine(self: *FileParser) ![]const u8 {
234         if (self.index >= self.text.len) return error.InvalidPassFailureReproducer;
235         const start = self.index;
236         while (self.index < self.text.len and self.text[self.index] != '\n') self.index += 1;
237         if (self.index >= self.text.len) return error.InvalidPassFailureReproducer;
238         const line = self.text[start..self.index];
239         self.index += 1;
240         return line;
241     }
242 };
243 
244 fn failingPass(ctx: *pass_mod.PassContext) pass_mod.PassResult {
245     _ = ctx;
246     return .failure;
247 }
248 
249 fn buildReplayRegistry(allocator: std.mem.Allocator) !pipeline_mod.PassRegistry {
250     var registry = pipeline_mod.PassRegistry.init(allocator);
251     errdefer registry.deinit();
252     try registry.registerPass(.{
253         .name = "choir-reproducer-test-fail",
254         .description = "test failure pass",
255         .pass = .{
256             .name = "choir-reproducer-test-fail",
257             .description = "test failure pass",
258             .run_fn = failingPass,
259             .mutation_scope = .read_only,
260         },
261     });
262     return registry;
263 }
264 
265 fn buildReplayModule(ctx: *ir.Context) !*ir.Operation {
266     const loc = ir.Location.getUnknown();
267     const module = try dialects.BuiltinDialect.ModuleOp.create(ctx, loc);
268     const body = module.getBodyBlock();
269     const i32_type = try dialects.ArithDialect.getScalarType(ctx, .i32);
270     for (0..2) |index| {
271         const name = try std.fmt.allocPrint(ir.context.transientAllocator(ctx), "reproducer_{d}", .{index});
272         var func = try dialects.FuncDialect.FuncOp.create(ctx, loc, name, &.{i32_type}, &.{i32_type});
273         try body.addOperation(func.op);
274         const ret = try dialects.FuncDialect.ReturnOp.create(ctx, loc, &.{func.getArgument(0)});
275         try func.getEntryBlock().addOperation(ret.op);
276     }
277     return module.op;
278 }
279 
280 fn initReplayContext(allocator: std.mem.Allocator) !ir.Context {
281     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
282     errdefer ctx.deinit(allocator);
283     try dialects.registerChoirDialect(&ctx);
284     _ = try ctx.getOrLoadDialect("builtin");
285     _ = try ctx.getOrLoadDialect("arith");
286     _ = try ctx.getOrLoadDialect("func");
287     return ctx;
288 }
289 
290 test "pass failure reproducer writes file and replays captured pipeline snapshot" {
291     const testing = std.testing;
292 
293     var arena_state = alloc_arena.Arena.init(testing.allocator);
294     defer arena_state.deinit();
295     const arena = arena_state.allocator();
296 
297     var registry = try buildReplayRegistry(arena);
298     defer registry.deinit();
299 
300     var ctx = try initReplayContext(arena);
301     defer ctx.deinit(arena);
302 
303     const module = try buildReplayModule(&ctx);
304     var manager = pass_mod.PassManager.init(arena);
305     defer manager.deinit();
306     try textual.parsePassPipeline(&registry, "func.func(choir-reproducer-test-fail)", &manager);
307 
308     const result = manager.runWithOptions(module, &ctx, .{ .max_threads = 2 });
309     try testing.expectEqual(pass_mod.PassResult.failure, result);
310     const reproducer = manager.getLastFailureReproducer() orelse return error.TestExpectedReproducer;
311 
312     const formatted = try formatPassFailureReproducerAlloc(arena, reproducer);
313     var parsed = try parsePassFailureReproducerAlloc(arena, formatted);
314     defer parsed.deinit(arena);
315     try testing.expectEqualStrings(reproducer.pipeline, parsed.pipeline);
316     try testing.expectEqualStrings(reproducer.ir, parsed.ir);
317     try testing.expectEqual(@as(usize, 2), parsed.max_threads);
318     try testing.expectEqual(pass_mod.PassFailureKind.pass, parsed.failure_kind.?);
319 
320     var tmp = testing.tmpDir(.{});
321     defer tmp.cleanup();
322     const path = try std.fs.path.join(arena, &.{ ".zig-cache", "tmp", tmp.sub_path[0..], "choir-pass.choirrepro" });
323     try writePassFailureReproducerFile(arena, path, reproducer);
324 
325     var file_storage: [32 * 1024]u8 = undefined;
326     const file_bytes = try std.Io.Dir.cwd().readFile(
327         std.Options.debug_io,
328         path,
329         &file_storage,
330     );
331     var from_file = try parsePassFailureReproducerAlloc(arena, file_bytes);
332     defer from_file.deinit(arena);
333 
334     var replay_ctx = try initReplayContext(arena);
335     defer replay_ctx.deinit(arena);
336     const replay = try replayPassFailureReproducer(arena, &registry, &replay_ctx, &from_file, .{});
337 
338     try testing.expectEqual(pass_mod.PassResult.failure, replay.result);
339     try testing.expectEqual(pass_mod.PassFailureKind.pass, replay.failure_kind.?);
340     try testing.expectEqual(@as(usize, 2), replay.worker_count);
341 }
342 
343 test "pass failure reproducer preserves exhaustion and refuses replay without its work budget" {
344     const allocator = std.testing.allocator;
345     const original = pass_mod.PassFailureReproducer{
346         .pipeline = "",
347         .ir = "",
348         .max_threads = 1,
349         .worker_count = 1,
350         .verifier_enabled = false,
351         .failure_kind = .exhausted,
352     };
353     const bytes = try formatPassFailureReproducerAlloc(allocator, &original);
354     defer allocator.free(bytes);
355     var parsed = try parsePassFailureReproducerAlloc(allocator, bytes);
356     defer parsed.deinit(allocator);
357     try std.testing.expectEqual(.exhausted, parsed.failure_kind.?);
358     var registry = pipeline_mod.PassRegistry.init(allocator);
359     defer registry.deinit();
360     var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
361     defer context.deinit(allocator);
362     try std.testing.expectError(
363         error.UnrecordedWorkBudget,
364         replayPassFailureReproducer(allocator, &registry, &context, &parsed, .{}),
365     );
366     try std.testing.expectEqual(0, context.operationCount());
367 }