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(®istry, "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, ®istry, &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, ®istry, &context, &parsed, .{}),
365 );
366 try std.testing.expectEqual(0, context.operationCount());
367 }