lib/choir/src/passes/pass/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_arena = @import("alloc_arena");
3 const alloc_observe = @import("alloc_observe");
4 const sys = @import("sys");
5 const choir = @import("../../root.zig");
6 const ir = @import("../../core/root.zig");
7 const diagnostics = choir.diagnostics;
8 const passes = @import("../root.zig");
9 const subject = @import("root.zig");
10 const instrumentation = passes.instrumentation;
11
12 const PassInstrumentor = instrumentation.PassInstrumentor;
13 const PassInfo = instrumentation.PassInfo;
14 const TimingInstrumentation = instrumentation.TimingInstrumentation;
15 const CountingInstrumentation = instrumentation.CountingInstrumentation;
16 const PassStatisticsInstrumentation = instrumentation.PassStatisticsInstrumentation;
17 const PassMutationScope = subject.PassMutationScope;
18 const dialectDependencies = subject.dialectDependencies;
19 const Pass = subject.Pass;
20 const analysisId = subject.analysisId;
21 const AnalysisDescriptor = subject.AnalysisDescriptor;
22 const Analysis = subject.Analysis;
23 const PassManagerStats = subject.PassManagerStats;
24 const PassFailureKind = subject.PassFailureKind;
25 const PreservedAnalyses = subject.PreservedAnalyses;
26 const AnalysisCache = subject.AnalysisCache;
27 const PassContext = subject.PassContext;
28 const PassResult = subject.PassResult;
29 const PipelineEntry = subject.PipelineEntry;
30 const OpPassManagerTargetKind = subject.OpPassManagerTargetKind;
31 const OpPassManager = subject.OpPassManager;
32 const PassManager = subject.PassManager;
33 const OperationPass = subject.OperationPass;
34
35 test {
36 std.testing.refAllDecls(subject);
37 }
38
39 fn test_pass_success(_: *PassContext) PassResult {
40 return .success;
41 }
42
43 fn test_pass_failure(_: *PassContext) PassResult {
44 return .failure;
45 }
46
47 test "IR context census counts each PassManager pass run once" {
48 const testing = std.testing;
49 const test_dialect = @import("../../dialects/fixture/root.zig");
50
51 var arena = alloc_arena.Arena.init(testing.allocator);
52 defer arena.deinit();
53 const allocator = arena.allocator();
54
55 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
56 defer ctx.deinit(allocator);
57 try ir.dialects.loadDialectSpec(&ctx, test_dialect.TestDialect.spec);
58 const module_op = try test_dialect.TestDialect.ModuleOp.create(
59 &ctx,
60 ir.Location.getUnknown(),
61 );
62
63 var normal = PassManager.init(allocator);
64 defer normal.deinit();
65 try normal.addPass(.{
66 .name = "context-census-success",
67 .description = "",
68 .run_fn = test_pass_success,
69 });
70 try testing.expectEqual(PassResult.success, normal.run(module_op.op, &ctx));
71 try testing.expectEqual(@as(u64, 1), ctx.passRunCount());
72 try testing.expectEqual(
73 PassResult.success,
74 normal.runWithOptions(module_op.op, &ctx, .{}),
75 );
76 try testing.expectEqual(@as(u64, 2), ctx.passRunCount());
77
78 var failing = PassManager.init(allocator);
79 defer failing.deinit();
80 try failing.addPass(.{
81 .name = "context-census-failure",
82 .description = "",
83 .run_fn = test_pass_failure,
84 });
85 try testing.expectEqual(PassResult.failure, failing.run(module_op.op, &ctx));
86 try testing.expectEqual(@as(u64, 3), ctx.passRunCount());
87
88 var fixed = PassManager.init(allocator);
89 defer fixed.deinit();
90 try fixed.addPass(.{
91 .name = "context-census-fixed-point",
92 .description = "",
93 .run_fn = test_pass_success,
94 });
95 const fixed_result = fixed.runToFixedPoint(module_op.op, &ctx, 8);
96 try testing.expectEqual(PassResult.success, fixed_result.result);
97 try testing.expectEqual(@as(usize, 1), fixed_result.iterations);
98 try testing.expectEqual(@as(u64, 4), ctx.passRunCount());
99 }
100
101 const OperationPassTestTargetOp = struct {
102 op: *ir.Operation,
103 pub const operation_name = ir.dialects.operationName("test", "target_op");
104 };
105
106 const OperationPassCounter = struct {
107 var count: usize = 0;
108
109 fn run_on_op(_: *OperationPassTestTargetOp, _: *PassContext) PassResult {
110 count += 1;
111 return .success;
112 }
113 };
114
115 const OperationPassTestFailOp = struct {
116 op: *ir.Operation,
117 pub const operation_name = ir.dialects.operationName("test", "fail_op");
118 };
119
120 const OperationPassFailCounter = struct {
121 var count: usize = 0;
122
123 fn run_on_op(_: *OperationPassTestFailOp, _: *PassContext) PassResult {
124 count += 1;
125 return .failure;
126 }
127 };
128
129 test "OperationPass walks and matches operations" {
130 const testing = std.testing;
131 const test_dialect = @import("../../dialects/fixture/root.zig");
132
133 var arena = alloc_arena.Arena.init(std.testing.allocator);
134 defer arena.deinit();
135 const allocator = arena.allocator();
136
137 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
138 defer ctx.deinit(allocator);
139 try ctx.allowUnregistered();
140
141 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
142 const module_block = module_op.getBodyBlock();
143
144 var builder = ir.OperationBuilder.init(&ctx);
145 const target_name = ir.dialects.operationName("test", "target_op");
146 const target1_state = ir.Operation.State.init(target_name, ir.Location.getUnknown());
147 const target1_op = try builder.create(target1_state);
148 try module_block.addOperation(target1_op);
149
150 const func_op = try test_dialect.TestDialect.FuncOp.create(&ctx, ir.Location.getUnknown(), "test_func", &.{});
151 try module_block.addOperation(func_op.op);
152
153 const func_block = func_op.getEntryBlock();
154
155 const target2_state = ir.Operation.State.init(target_name, ir.Location.getUnknown());
156 const target2_op = try builder.create(target2_state);
157 try func_block.addOperation(target2_op);
158
159 const other_state = ir.Operation.State.init("test.other_op", ir.Location.getUnknown());
160 const other_op = try builder.create(other_state);
161 try func_block.addOperation(other_op);
162
163 OperationPassCounter.count = 0;
164
165 const TestPass = OperationPass(OperationPassTestTargetOp, OperationPassCounter.run_on_op);
166 const pass = TestPass.init("test-pass", "Test pass for counting target ops");
167
168 var analysis_cache = AnalysisCache.init(allocator, null);
169 defer analysis_cache.deinit();
170
171 var pass_ctx = PassContext.init(module_op.op, &ctx, allocator, &analysis_cache);
172 defer pass_ctx.deinit();
173 const result = pass.base.run(&pass_ctx);
174
175 try testing.expectEqual(PassResult.success, result);
176 try testing.expectEqual(@as(usize, 2), OperationPassCounter.count);
177 }
178
179 test "OperationPass stops on failure" {
180 const testing = std.testing;
181 const test_dialect = @import("../../dialects/fixture/root.zig");
182
183 var arena = alloc_arena.Arena.init(std.testing.allocator);
184 defer arena.deinit();
185 const allocator = arena.allocator();
186
187 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
188 defer ctx.deinit(allocator);
189 try ctx.allowUnregistered();
190
191 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
192 const module_block = module_op.getBodyBlock();
193
194 var builder = ir.OperationBuilder.init(&ctx);
195 const target_name = ir.dialects.operationName("test", "fail_op");
196 const target1_state = ir.Operation.State.init(target_name, ir.Location.getUnknown());
197 const target1_op = try builder.create(target1_state);
198 try module_block.addOperation(target1_op);
199
200 const target2_state = ir.Operation.State.init(target_name, ir.Location.getUnknown());
201 const target2_op = try builder.create(target2_state);
202 try module_block.addOperation(target2_op);
203
204 OperationPassFailCounter.count = 0;
205
206 const FailPass = OperationPass(OperationPassTestFailOp, OperationPassFailCounter.run_on_op);
207 const pass = FailPass.init("fail-pass", "Pass that always fails");
208
209 var analysis_cache = AnalysisCache.init(allocator, null);
210 defer analysis_cache.deinit();
211
212 var pass_ctx = PassContext.init(module_op.op, &ctx, allocator, &analysis_cache);
213 defer pass_ctx.deinit();
214 const result = pass.base.run(&pass_ctx);
215
216 try testing.expectEqual(PassResult.failure, result);
217 try testing.expectEqual(@as(usize, 1), OperationPassFailCounter.count);
218 }
219
220 fn preserveCompileTimeTestAnalysisSet(preserved: *PreservedAnalyses) void {
221 preserved.preserveAnalysisSet(&.{
222 analysisId("analysis.test.static.a"),
223 analysisId("analysis.test.static.c"),
224 });
225 }
226
227 test "PreservedAnalyses borrows compile-time analysis sets" {
228 const testing = std.testing;
229 const test_dialect = @import("../../dialects/fixture/root.zig");
230 const desc_a = AnalysisDescriptor{
231 .id = analysisId("analysis.test.static.a"),
232 .name = "analysis.test.static.a",
233 };
234 const desc_b = AnalysisDescriptor{
235 .id = analysisId("analysis.test.static.b"),
236 .name = "analysis.test.static.b",
237 };
238 const desc_c = AnalysisDescriptor{
239 .id = analysisId("analysis.test.static.c"),
240 .name = "analysis.test.static.c",
241 };
242
243 var arena = alloc_arena.Arena.init(testing.allocator);
244 defer arena.deinit();
245 const allocator = arena.allocator();
246
247 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
248 defer ctx.deinit(allocator);
249 const module_op = try test_dialect.TestDialect.ModuleOp.create(
250 &ctx,
251 ir.Location.getUnknown(),
252 );
253
254 var stats: PassManagerStats = .{};
255 var analysis_cache = AnalysisCache.init(allocator, &stats);
256 defer analysis_cache.deinit();
257 var pass_ctx = PassContext.init(
258 module_op.op,
259 &ctx,
260 allocator,
261 &analysis_cache,
262 );
263 defer pass_ctx.deinit();
264
265 analysis_counter = 0;
266 _ = try pass_ctx.getAnalysis(module_op.op, &desc_a, computeCounter, cleanupCounter);
267 _ = try pass_ctx.getAnalysis(module_op.op, &desc_b, computeCounter, cleanupCounter);
268 _ = try pass_ctx.getAnalysis(module_op.op, &desc_c, computeCounter, cleanupCounter);
269
270 var failing = testing.FailingAllocator.init(testing.allocator, .{ .fail_index = 0 });
271 var preserved = PreservedAnalyses.init(failing.allocator());
272 defer preserved.deinit();
273
274 preserveCompileTimeTestAnalysisSet(&preserved);
275
276 analysis_cache.invalidate(&preserved);
277 const a = try pass_ctx.getAnalysis(module_op.op, &desc_a, computeCounter, cleanupCounter);
278 const b = try pass_ctx.getAnalysis(module_op.op, &desc_b, computeCounter, cleanupCounter);
279 const c = try pass_ctx.getAnalysis(module_op.op, &desc_c, computeCounter, cleanupCounter);
280 try testing.expectEqual(@as(u32, 1), @as(*u32, @ptrCast(@alignCast(a))).*);
281 try testing.expectEqual(@as(u32, 4), @as(*u32, @ptrCast(@alignCast(b))).*);
282 try testing.expectEqual(@as(u32, 3), @as(*u32, @ptrCast(@alignCast(c))).*);
283 try testing.expectEqual(@as(u64, 1), stats.analyses_invalidated);
284 try preserved.preserveAnalysis(desc_a.id);
285 try testing.expectEqual(@as(usize, 0), failing.alloc_index);
286 try testing.expectError(error.OutOfMemory, preserved.preserveAnalysis(desc_b.id));
287 }
288
289 test "AnalysisCache invalidates based on preserved interfaces" {
290 const testing = std.testing;
291 const test_dialect = @import("../../dialects/fixture/root.zig");
292
293 var arena = alloc_arena.Arena.init(std.testing.allocator);
294 defer arena.deinit();
295 const allocator = arena.allocator();
296
297 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
298 defer ctx.deinit(allocator);
299
300 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
301
302 var stats: PassManagerStats = .{};
303 var analysis_cache = AnalysisCache.init(allocator, &stats);
304 defer analysis_cache.deinit();
305
306 var pass_ctx = PassContext.init(module_op.op, &ctx, allocator, &analysis_cache);
307 defer pass_ctx.deinit();
308
309 const desc_a = AnalysisDescriptor{
310 .id = analysisId("analysis.test.a"),
311 .name = "analysis.test.a",
312 };
313 const desc_b = AnalysisDescriptor{
314 .id = analysisId("analysis.test.b"),
315 .name = "analysis.test.b",
316 .required_interfaces = &.{ir.interfaces.EffectOpInterface.id},
317 };
318 const desc_c = AnalysisDescriptor{
319 .id = analysisId("analysis.test.c"),
320 .name = "analysis.test.c",
321 };
322
323 analysis_counter = 0;
324 const a_ptr1 = try pass_ctx.getAnalysis(module_op.op, &desc_a, computeCounter, cleanupCounter);
325 const b_ptr1 = try pass_ctx.getAnalysis(module_op.op, &desc_b, computeCounter, cleanupCounter);
326 const c_ptr1 = try pass_ctx.getAnalysis(module_op.op, &desc_c, computeCounter, cleanupCounter);
327
328 try testing.expectEqual(@as(u32, 1), @as(*u32, @ptrCast(@alignCast(a_ptr1))).*);
329 try testing.expectEqual(@as(u32, 2), @as(*u32, @ptrCast(@alignCast(b_ptr1))).*);
330 try testing.expectEqual(@as(u32, 3), @as(*u32, @ptrCast(@alignCast(c_ptr1))).*);
331
332 var preserved = PreservedAnalyses.init(allocator);
333 defer preserved.deinit();
334 try preserved.preserveInterface(ir.interfaces.EffectOpInterface.id);
335
336 analysis_cache.invalidate(&preserved);
337
338 const a_ptr2 = try pass_ctx.getAnalysis(module_op.op, &desc_a, computeCounter, cleanupCounter);
339 const b_ptr2 = try pass_ctx.getAnalysis(module_op.op, &desc_b, computeCounter, cleanupCounter);
340 const c_ptr2 = try pass_ctx.getAnalysis(module_op.op, &desc_c, computeCounter, cleanupCounter);
341
342 try testing.expectEqual(@as(u32, 4), @as(*u32, @ptrCast(@alignCast(a_ptr2))).*);
343 try testing.expectEqual(@as(u32, 2), @as(*u32, @ptrCast(@alignCast(b_ptr2))).*);
344 try testing.expectEqual(@as(u32, 5), @as(*u32, @ptrCast(@alignCast(c_ptr2))).*);
345 try testing.expectEqual(@as(u64, 2), stats.analyses_invalidated);
346 }
347
348 test "PassContext instruments analysis computation misses" {
349 const testing = std.testing;
350 const test_dialect = @import("../../dialects/fixture/root.zig");
351
352 var arena = alloc_arena.Arena.init(std.testing.allocator);
353 defer arena.deinit();
354 const allocator = arena.allocator();
355
356 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
357 defer ctx.deinit(allocator);
358
359 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
360
361 var stats: PassManagerStats = .{};
362 var analysis_cache = AnalysisCache.init(allocator, &stats);
363 defer analysis_cache.deinit();
364
365 var counter = CountingInstrumentation{};
366 var timing = TimingInstrumentation.init(allocator);
367 defer timing.deinit();
368
369 var instrumentor = PassInstrumentor.init(allocator);
370 defer instrumentor.deinit();
371 try instrumentor.addInstrumentation(counter.instrumentation());
372 try instrumentor.addInstrumentation(timing.instrumentation());
373
374 var pass_ctx = PassContext.initWithInstrumentor(
375 module_op.op,
376 &ctx,
377 allocator,
378 &analysis_cache,
379 &instrumentor,
380 );
381 defer pass_ctx.deinit();
382
383 const desc = AnalysisDescriptor{
384 .id = analysisId("analysis.test.instrumented"),
385 .name = "analysis.test.instrumented",
386 };
387
388 analysis_counter = 0;
389 const first = try pass_ctx.getAnalysis(module_op.op, &desc, computeCounter, cleanupCounter);
390 const second = try pass_ctx.getAnalysis(module_op.op, &desc, computeCounter, cleanupCounter);
391
392 try testing.expectEqual(first, second);
393 try testing.expectEqual(@as(usize, 1), counter.analysis_count);
394 try testing.expectEqual(@as(u64, 1), stats.analysis_misses);
395 try testing.expectEqual(@as(u64, 1), stats.analysis_hits);
396 try testing.expectEqual(@as(u64, 1), timing.getAnalysisCount(desc.name).?);
397 try testing.expect(timing.getAnalysisTime(desc.name) != null);
398 }
399
400 test "Analysis wraps typed compute and cleanup callbacks" {
401 const testing = std.testing;
402 const test_dialect = @import("../../dialects/fixture/root.zig");
403
404 var arena = alloc_arena.Arena.init(std.testing.allocator);
405 defer arena.deinit();
406 const allocator = arena.allocator();
407
408 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
409 defer ctx.deinit(allocator);
410
411 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
412
413 var stats: PassManagerStats = .{};
414 typed_analysis_counter = 0;
415 typed_analysis_cleanup_count = 0;
416
417 {
418 var analysis_cache = AnalysisCache.init(allocator, &stats);
419 defer analysis_cache.deinit();
420
421 var pass_ctx = PassContext.init(module_op.op, &ctx, allocator, &analysis_cache);
422 defer pass_ctx.deinit();
423
424 try testing.expectEqual(analysisId("analysis.test.typed"), TypedCounterAnalysis.id);
425 try testing.expect(TypedCounterAnalysis.value_type == u32);
426 try testing.expectEqualStrings("analysis.test.typed", TypedCounterAnalysis.descriptor.name);
427
428 const first = try TypedCounterAnalysis.get(&pass_ctx, module_op.op);
429 const second = try TypedCounterAnalysis.get(&pass_ctx, module_op.op);
430
431 try testing.expectEqual(first, second);
432 try testing.expectEqual(@as(u32, 1), first.*);
433 try testing.expectEqual(@as(u64, 1), stats.analysis_misses);
434 try testing.expectEqual(@as(u64, 1), stats.analysis_hits);
435
436 var preserved = PreservedAnalyses.init(allocator);
437 defer preserved.deinit();
438 analysis_cache.invalidate(&preserved);
439
440 try testing.expectEqual(@as(u32, 1), typed_analysis_cleanup_count);
441
442 const third = try TypedCounterAnalysis.get(&pass_ctx, module_op.op);
443 try testing.expectEqual(@as(u32, 2), third.*);
444
445 try TypedCounterAnalysis.preserve(&pass_ctx);
446 analysis_cache.invalidate(&pass_ctx.preserved);
447 try testing.expectEqual(@as(u32, 1), typed_analysis_cleanup_count);
448 try testing.expectEqual(third, try TypedCounterAnalysis.get(&pass_ctx, module_op.op));
449 }
450
451 try testing.expectEqual(@as(u32, 2), typed_analysis_cleanup_count);
452 }
453
454 test "AnalysisCache cleans computed values after insertion failure" {
455 const testing = std.testing;
456 const test_dialect = @import("../../dialects/fixture/root.zig");
457 var ctx = try ir.Context.init(testing.allocator, ir.Context.Limits.testing);
458 defer ctx.deinit(testing.allocator);
459 const module = try test_dialect.TestDialect.ModuleOp.create(&ctx, .unknown);
460 var failing = testing.FailingAllocator.init(testing.allocator, .{ .fail_index = 1 });
461 var stats: PassManagerStats = .{};
462 typed_analysis_counter = 0;
463 typed_analysis_cleanup_count = 0;
464 {
465 var cache = AnalysisCache.init(failing.allocator(), &stats);
466 defer cache.deinit();
467 var pass_ctx = PassContext.init(module.op, &ctx, failing.allocator(), &cache);
468 defer pass_ctx.deinit();
469
470 try testing.expectError(error.OutOfMemory, TypedCounterAnalysis.get(&pass_ctx, module.op));
471 try testing.expect(failing.has_induced_failure);
472 try testing.expectEqual(@as(u32, 1), typed_analysis_counter);
473 try testing.expectEqual(@as(u32, 1), typed_analysis_cleanup_count);
474 try testing.expectEqual(@as(usize, 0), cache.entries.count());
475 try testing.expectEqual(@as(u64, 0), stats.analysis_misses);
476
477 failing.fail_index = std.math.maxInt(usize);
478 const fresh = try TypedCounterAnalysis.get(&pass_ctx, module.op);
479 try testing.expectEqual(@as(u32, 2), fresh.*);
480 try testing.expectEqual(@as(u32, 2), typed_analysis_counter);
481 try testing.expectEqual(@as(u32, 1), typed_analysis_cleanup_count);
482 try testing.expectEqual(@as(usize, 1), cache.entries.count());
483 try testing.expectEqual(@as(u64, 1), stats.analysis_misses);
484 try testing.expectEqual(fresh, try TypedCounterAnalysis.get(&pass_ctx, module.op));
485 try testing.expectEqual(@as(u32, 2), typed_analysis_counter);
486 try testing.expectEqual(@as(u64, 1), stats.analysis_hits);
487 }
488 try testing.expectEqual(@as(u32, 2), typed_analysis_cleanup_count);
489 }
490
491 const OperationVersionAnalysis = Analysis(
492 i64,
493 "analysis.test.operation-version",
494 &.{},
495 computeOperationVersion,
496 cleanupOperationVersion,
497 null,
498 );
499
500 fn computeOperationVersion(ctx: *PassContext, op: *ir.Operation) anyerror!*i64 {
501 const version = op.getAttr("analysis_version").?.cast(ir.Attribute.IntegerAttr).?.value;
502 const value = try ctx.allocator.create(i64);
503 value.* = version;
504 return value;
505 }
506
507 fn cleanupOperationVersion(value: *i64, allocator: std.mem.Allocator) void {
508 allocator.destroy(value);
509 }
510
511 const MutatingFailurePass = struct {
512 fn run(ctx: *PassContext) PassResult {
513 const version = ctx.ir_ctx.getI64Attr(2) catch return .failure;
514 ctx.op.setAttr("analysis_version", version) catch return .failure;
515 ctx.markModified();
516 return .failure;
517 }
518
519 fn preserving(ctx: *PassContext) PassResult {
520 ctx.preserveAllAnalyses();
521 return run(ctx);
522 }
523 };
524
525 test "PassManager failing mutations honor analysis preservation" {
526 try expectFailedMutationCache(false);
527 try expectFailedMutationCache(true);
528 }
529
530 fn expectFailedMutationCache(preserve_all: bool) !void {
531 const testing = std.testing;
532 const allocator = testing.allocator;
533 const test_dialect = @import("../../dialects/fixture/root.zig");
534 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
535 defer ctx.deinit(allocator);
536 try ir.dialects.loadDialectSpec(&ctx, test_dialect.TestDialect.spec);
537 const module = try test_dialect.TestDialect.ModuleOp.create(&ctx, .unknown);
538 try module.op.setAttr("analysis_version", try ctx.getI64Attr(1));
539 _ = try ctx.getI64Attr(2);
540 var stats: PassManagerStats = .{};
541 var cache = AnalysisCache.init(allocator, &stats);
542 defer cache.deinit();
543 var pass_ctx = PassContext.init(module.op, &ctx, allocator, &cache);
544 defer pass_ctx.deinit();
545 const before = (try OperationVersionAnalysis.get(&pass_ctx, module.op)).*;
546 try testing.expectEqual(@as(i64, 1), before);
547 var pm = PassManager.init(allocator);
548 defer pm.deinit();
549 var counter = CountingInstrumentation{};
550 try pm.addInstrumentation(counter.instrumentation());
551 try pm.addPass(.{
552 .name = "mutating-failure",
553 .description = "",
554 .run_fn = if (preserve_all) MutatingFailurePass.preserving else MutatingFailurePass.run,
555 });
556
557 try testing.expectEqual(.failure, pm.runWithAnalysisCache(module.op, &ctx, &cache, .{}));
558 try testing.expectEqual(@as(u64, 1), pm.stats.pass_failures);
559 try testing.expectEqual(@as(u64, 1), pm.stats.passes_modified);
560 try testing.expectEqual(@as(usize, 1), counter.pass_failures);
561 const invalidated: u64 = if (preserve_all) 0 else 1;
562 try testing.expectEqual(invalidated, pm.stats.analyses_invalidated);
563 try testing.expect(cache.stats == &stats);
564 const observed = module.op.getAttr("analysis_version").?.cast(ir.Attribute.IntegerAttr).?.value;
565 try testing.expectEqual(@as(i64, 2), observed);
566 const after = (try OperationVersionAnalysis.get(&pass_ctx, module.op)).*;
567 try testing.expectEqual(if (preserve_all) before else observed, after);
568 const misses: u64 = if (preserve_all) 1 else 2;
569 const hits: u64 = if (preserve_all) 1 else 0;
570 try testing.expectEqual(misses, stats.analysis_misses);
571 try testing.expectEqual(hits, stats.analysis_hits);
572 }
573
574 var analysis_counter: u32 = 0;
575
576 fn computeCounter(ctx: *PassContext, _: *ir.Operation) anyerror!*anyopaque {
577 analysis_counter += 1;
578 const ptr = try ctx.allocator.create(u32);
579 ptr.* = analysis_counter;
580 return @ptrCast(ptr);
581 }
582
583 fn cleanupCounter(value: *anyopaque, allocator: std.mem.Allocator) void {
584 const ptr: *u32 = @ptrCast(@alignCast(value));
585 allocator.destroy(ptr);
586 }
587
588 const TypedCounterAnalysis = Analysis(
589 u32,
590 "analysis.test.typed",
591 &.{},
592 computeTypedCounter,
593 cleanupTypedCounter,
594 null,
595 );
596
597 var typed_analysis_counter: u32 = 0;
598 var typed_analysis_cleanup_count: u32 = 0;
599
600 fn computeTypedCounter(ctx: *PassContext, _: *ir.Operation) anyerror!*u32 {
601 typed_analysis_counter += 1;
602 const ptr = try ctx.allocator.create(u32);
603 ptr.* = typed_analysis_counter;
604 return ptr;
605 }
606
607 fn cleanupTypedCounter(value: *u32, allocator: std.mem.Allocator) void {
608 typed_analysis_cleanup_count += 1;
609 allocator.destroy(value);
610 }
611
612 const CallerCacheAnalysisRunner = struct {
613 fn run(pass_ctx: *PassContext) PassResult {
614 _ = TypedCounterAnalysis.get(pass_ctx, pass_ctx.op) catch return .failure;
615 pass_ctx.preserveAllAnalyses();
616 return .success;
617 }
618 };
619
620 test "PassManager.runWithAnalysisCache leaves caller cache reusable" {
621 const testing = std.testing;
622 const test_dialect = @import("../../dialects/fixture/root.zig");
623
624 const allocator = testing.allocator;
625
626 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
627 defer ctx.deinit(allocator);
628 try ir.dialects.loadDialectSpec(&ctx, test_dialect.TestDialect.spec);
629
630 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
631
632 typed_analysis_counter = 0;
633 typed_analysis_cleanup_count = 0;
634
635 var cache = AnalysisCache.init(allocator, null);
636 defer cache.deinit();
637
638 var pm = PassManager.init(allocator);
639 defer pm.deinit();
640 try pm.addPass(.{
641 .name = "caller-cache-analysis",
642 .description = "",
643 .run_fn = CallerCacheAnalysisRunner.run,
644 .mutation_scope = .read_only,
645 });
646
647 try testing.expectEqual(PassResult.success, pm.runWithAnalysisCache(module_op.op, &ctx, &cache, .{}));
648 try testing.expect(cache.stats == null);
649 try testing.expectEqual(@as(u64, 1), pm.stats.analysis_misses);
650 try testing.expectEqual(@as(u32, 1), typed_analysis_counter);
651
652 var pass_ctx = PassContext.init(module_op.op, &ctx, allocator, &cache);
653 defer pass_ctx.deinit();
654 const value = try TypedCounterAnalysis.get(&pass_ctx, module_op.op);
655
656 try testing.expectEqual(@as(u32, 1), value.*);
657 try testing.expectEqual(@as(u32, 1), typed_analysis_counter);
658 try testing.expectEqual(@as(u32, 0), typed_analysis_cleanup_count);
659 }
660
661 fn loadPassDependencyTestDialect(ctx: *ir.Context) !void {
662 try ir.dialects.loadDialectSpec(ctx, .{ .name = "passdep" });
663 }
664
665 fn loadFixedPointDependencyTestDialect(ctx: *ir.Context) !void {
666 try ir.dialects.loadDialectSpec(ctx, .{ .name = "fixeddep" });
667 }
668
669 fn appendInvalidTestConstant(ctx: *PassContext) PassResult {
670 const test_dialect = @import("../../dialects/fixture/root.zig");
671
672 const module = test_dialect.TestDialect.ModuleOp{ .op = ctx.op };
673 appendInvalidTestConstantToBlock(ctx.ir_ctx, module.getBodyBlock()) catch return .failure;
674 ctx.markModified();
675 return .success;
676 }
677
678 fn appendInvalidTestConstantToBlock(ctx: *ir.Context, block: *ir.Block) !void {
679 const test_dialect = @import("../../dialects/fixture/root.zig");
680
681 const i32_type = try test_dialect.TestDialect.getI32Type(ctx);
682 var builder = ir.OperationBuilder.init(ctx);
683 var state = ir.Operation.State.init(test_dialect.TestDialect.ConstantOp.operation_name, ir.Location.getUnknown());
684 state.addTypes(&.{i32_type});
685 const bad_op = try builder.create(state);
686 try block.addOperation(bad_op);
687 }
688
689 test "OpPassManager basic nesting" {
690 const testing = std.testing;
691 const allocator = testing.allocator;
692
693 var pm = OpPassManager.init(allocator, null);
694 defer pm.deinit();
695
696 try testing.expectEqual(@as(?[]const u8, null), pm.target_op_name);
697 try testing.expectEqual(OpPassManagerTargetKind.root, pm.target_kind);
698 try testing.expectEqual(@as(usize, 0), pm.getNestingDepth());
699
700 const func_pm = try pm.nest("func.func");
701 try testing.expectEqualStrings("func.func", func_pm.target_op_name.?);
702 try testing.expectEqual(OpPassManagerTargetKind.op, func_pm.target_kind);
703 try testing.expectEqual(@as(usize, 1), func_pm.getNestingDepth());
704 try testing.expectEqual(&pm, func_pm.parent.?);
705
706 const loop_pm = try func_pm.nest("scf.for");
707 try testing.expectEqualStrings("scf.for", loop_pm.target_op_name.?);
708 try testing.expectEqual(@as(usize, 2), loop_pm.getNestingDepth());
709 try testing.expectEqual(func_pm, loop_pm.parent.?);
710 }
711
712 test "OpPassManager supports op-agnostic nested manager" {
713 const testing = std.testing;
714 const allocator = testing.allocator;
715
716 var pm = OpPassManager.init(allocator, null);
717 defer pm.deinit();
718
719 const any_pm = try pm.nestAny();
720 try testing.expectEqual(@as(?[]const u8, null), any_pm.target_op_name);
721 try testing.expectEqual(OpPassManagerTargetKind.any, any_pm.target_kind);
722 try testing.expectEqual(@as(usize, 1), any_pm.getNestingDepth());
723 try testing.expectEqual(&pm, any_pm.parent.?);
724
725 try testing.expectEqual(@as(usize, 1), pm.pipeline.items.len);
726 try testing.expectEqual(PipelineEntry{ .nested = any_pm }, pm.pipeline.items[0]);
727 }
728
729 test "PassManager.nest creates nested pipeline" {
730 const testing = std.testing;
731 const allocator = testing.allocator;
732
733 var pm = PassManager.init(allocator);
734 defer pm.deinit();
735
736 const func_pm = try pm.nest("func.func");
737 try testing.expectEqualStrings("func.func", func_pm.target_op_name.?);
738
739 try testing.expectEqual(@as(usize, 1), pm.root.pipeline.items.len);
740 try testing.expectEqual(PipelineEntry{ .nested = func_pm }, pm.root.pipeline.items[0]);
741 }
742
743 test "PassManager.nestAny creates op-agnostic nested pipeline" {
744 const testing = std.testing;
745 const allocator = testing.allocator;
746
747 var pm = PassManager.init(allocator);
748 defer pm.deinit();
749
750 const any_pm = try pm.nestAny();
751 try testing.expectEqual(OpPassManagerTargetKind.any, any_pm.target_kind);
752 try testing.expectEqual(@as(?[]const u8, null), any_pm.target_op_name);
753
754 try testing.expectEqual(@as(usize, 1), pm.root.pipeline.items.len);
755 try testing.expectEqual(PipelineEntry{ .nested = any_pm }, pm.root.pipeline.items[0]);
756 }
757
758 const UnregisteredTargetCounter = struct {
759 var count: usize = 0;
760
761 fn run(_: *PassContext) PassResult {
762 count += 1;
763 return .success;
764 }
765 };
766
767 test "PassManager rejects unregistered nested pass targets" {
768 const testing = std.testing;
769 const test_dialect = @import("../../dialects/fixture/root.zig");
770
771 var arena = alloc_arena.Arena.init(std.testing.allocator);
772 defer arena.deinit();
773 const allocator = arena.allocator();
774
775 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
776 defer ctx.deinit(allocator);
777 try ctx.allowUnregistered();
778
779 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
780 const module_block = module_op.getBodyBlock();
781
782 var builder = ir.OperationBuilder.init(&ctx);
783 const state = ir.Operation.State.init("test.unregistered_target", ir.Location.getUnknown());
784 const target = try builder.create(state);
785 try module_block.addOperation(target);
786
787 UnregisteredTargetCounter.count = 0;
788
789 var pm = PassManager.init(allocator);
790 defer pm.deinit();
791
792 const nested = try pm.nest("test.unregistered_target");
793 try nested.addPass(.{
794 .name = "must-not-run",
795 .description = "",
796 .run_fn = UnregisteredTargetCounter.run,
797 });
798
799 try testing.expectEqual(PassResult.failure, pm.run(module_op.op, &ctx));
800 try testing.expectEqual(@as(usize, 0), UnregisteredTargetCounter.count);
801 }
802
803 const NonIsolatedTargetCounter = struct {
804 var count: usize = 0;
805
806 fn run(_: *PassContext) PassResult {
807 count += 1;
808 return .success;
809 }
810 };
811
812 test "PassManager rejects non-isolated nested pass targets" {
813 const testing = std.testing;
814 const test_dialect = @import("../../dialects/fixture/root.zig");
815
816 var arena = alloc_arena.Arena.init(std.testing.allocator);
817 defer arena.deinit();
818 const allocator = arena.allocator();
819
820 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
821 defer ctx.deinit(allocator);
822
823 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
824 const module_block = module_op.getBodyBlock();
825
826 _ = try ctx.registerOperation("test.non_isolated_target", .{});
827
828 var builder = ir.OperationBuilder.init(&ctx);
829 const state = ir.Operation.State.init("test.non_isolated_target", ir.Location.getUnknown());
830 const target = try builder.create(state);
831 try module_block.addOperation(target);
832
833 NonIsolatedTargetCounter.count = 0;
834
835 var pm = PassManager.init(allocator);
836 defer pm.deinit();
837
838 const nested = try pm.nest("test.non_isolated_target");
839 try nested.addPass(.{
840 .name = "must-not-run",
841 .description = "",
842 .run_fn = NonIsolatedTargetCounter.run,
843 });
844
845 try testing.expectEqual(PassResult.failure, pm.run(module_op.op, &ctx));
846 try testing.expectEqual(@as(usize, 0), NonIsolatedTargetCounter.count);
847 }
848
849 const AnyTargetCounter = struct {
850 var count: usize = 0;
851 var saw_func = false;
852
853 fn run(ctx: *PassContext) PassResult {
854 const dialect = @import("../../dialects/fixture/root.zig");
855 count += 1;
856 if (std.mem.eql(u8, ctx.op.name.name, dialect.TestDialect.FuncOp.operation_name)) {
857 saw_func = true;
858 }
859 return .success;
860 }
861 };
862
863 test "PassManager.nestAny runs registered isolated targets and skips non-viable operations" {
864 const testing = std.testing;
865 const test_dialect = @import("../../dialects/fixture/root.zig");
866
867 var arena = alloc_arena.Arena.init(std.testing.allocator);
868 defer arena.deinit();
869 const allocator = arena.allocator();
870
871 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
872 defer ctx.deinit(allocator);
873 try ctx.allowUnregistered();
874
875 const loc = ir.Location.getUnknown();
876 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
877 const module_block = module_op.getBodyBlock();
878
879 _ = try ctx.registerOperation("test.non_isolated_target", .{});
880
881 var builder = ir.OperationBuilder.init(&ctx);
882 const unregistered_state = ir.Operation.State.init("test.unregistered_target", loc);
883 const unregistered = try builder.create(unregistered_state);
884 try module_block.addOperation(unregistered);
885
886 const non_isolated_state = ir.Operation.State.init("test.non_isolated_target", loc);
887 const non_isolated = try builder.create(non_isolated_state);
888 try module_block.addOperation(non_isolated);
889
890 const func_op = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "any_target", &.{});
891 try module_block.addOperation(func_op.op);
892
893 AnyTargetCounter.count = 0;
894 AnyTargetCounter.saw_func = false;
895
896 var pm = PassManager.init(allocator);
897 defer pm.deinit();
898
899 const any_pm = try pm.nestAny();
900 try any_pm.addPass(.{
901 .name = "any-counter",
902 .description = "counts any isolated target",
903 .run_fn = AnyTargetCounter.run,
904 });
905
906 try testing.expectEqual(PassResult.success, pm.run(module_op.op, &ctx));
907 try testing.expectEqual(@as(usize, 1), AnyTargetCounter.count);
908 try testing.expect(AnyTargetCounter.saw_func);
909 }
910
911 test "OpPassManager collects dependent dialects from nested pipelines" {
912 const testing = std.testing;
913 const allocator = testing.allocator;
914
915 var pm = PassManager.init(allocator);
916 defer pm.deinit();
917
918 try pm.addPass(.{
919 .name = "root-dependent-pass",
920 .description = "",
921 .run_fn = test_pass_success,
922 .dependent_dialects = dialectDependencies(&.{ "firstdep", "shareddep" }),
923 });
924
925 const nested = try pm.nest("test.func");
926 try nested.addPass(.{
927 .name = "nested-dependent-pass",
928 .description = "",
929 .run_fn = test_pass_success,
930 .dependent_dialects = dialectDependencies(&.{ "shareddep", "nesteddep" }),
931 });
932
933 var names: std.ArrayListUnmanaged([]const u8) = .empty;
934 defer names.deinit(allocator);
935 try pm.root.collectDependentDialects(allocator, &names);
936
937 try testing.expectEqual(@as(usize, 3), names.items.len);
938 try testing.expectEqualStrings("firstdep", names.items[0]);
939 try testing.expectEqualStrings("shareddep", names.items[1]);
940 try testing.expectEqualStrings("nesteddep", names.items[2]);
941 }
942
943 const DependentDialectRunner = struct {
944 var saw_loaded = false;
945
946 fn run(ctx: *PassContext) PassResult {
947 saw_loaded = ctx.ir_ctx.isDialectLoaded("passdep");
948 ctx.preserveAllAnalyses();
949 return if (saw_loaded) .success else .failure;
950 }
951 };
952
953 test "PassManager preloads dependent dialects before running passes" {
954 const testing = std.testing;
955 const test_dialect = @import("../../dialects/fixture/root.zig");
956
957 var arena = alloc_arena.Arena.init(std.testing.allocator);
958 defer arena.deinit();
959 const allocator = arena.allocator();
960
961 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
962 defer ctx.deinit(allocator);
963 try ir.dialects.loadDialectSpec(&ctx, test_dialect.TestDialect.spec);
964
965 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
966 try ctx.requireRegistered();
967 try ctx.registerDialectLoader("passdep", loadPassDependencyTestDialect);
968
969 DependentDialectRunner.saw_loaded = false;
970
971 var pm = PassManager.init(allocator);
972 defer pm.deinit();
973 try pm.addPass(.{
974 .name = "dependent-dialect-runner",
975 .description = "",
976 .run_fn = DependentDialectRunner.run,
977 .mutation_scope = .read_only,
978 .dependent_dialects = dialectDependencies(&.{"passdep"}),
979 });
980
981 try testing.expect(!ctx.isDialectLoaded("passdep"));
982 try testing.expectEqual(PassResult.success, pm.run(module_op.op, &ctx));
983 try testing.expect(DependentDialectRunner.saw_loaded);
984 try testing.expect(ctx.isDialectLoaded("passdep"));
985 }
986
987 const VerifierFailureFollower = struct {
988 var runs: usize = 0;
989
990 fn run(_: *PassContext) PassResult {
991 runs += 1;
992 return .success;
993 }
994 };
995
996 test "PassManager verifier stops pipeline after invalid IR" {
997 const testing = std.testing;
998 const test_dialect = @import("../../dialects/fixture/root.zig");
999
1000 var arena = alloc_arena.Arena.init(std.testing.allocator);
1001 defer arena.deinit();
1002 const allocator = arena.allocator();
1003
1004 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1005 defer ctx.deinit(allocator);
1006 try ir.dialects.loadDialectSpec(&ctx, test_dialect.TestDialect.spec);
1007
1008 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
1009
1010 VerifierFailureFollower.runs = 0;
1011
1012 var pm = PassManager.init(allocator);
1013 defer pm.deinit();
1014 pm.enableVerifier();
1015 try pm.addPass(.{
1016 .name = "append-invalid-constant",
1017 .description = "",
1018 .run_fn = appendInvalidTestConstant,
1019 });
1020 try pm.addPass(.{
1021 .name = "must-not-run-after-verifier-failure",
1022 .description = "",
1023 .run_fn = VerifierFailureFollower.run,
1024 });
1025
1026 try testing.expect(pm.verifierEnabled());
1027 try testing.expectEqual(PassResult.failure, pm.run(module_op.op, &ctx));
1028 try testing.expectEqual(@as(u64, 1), pm.stats.pass_runs);
1029 try testing.expectEqual(@as(u64, 0), pm.stats.pass_failures);
1030 try testing.expectEqual(@as(u64, 1), pm.stats.verifier_failures);
1031 try testing.expectEqual(@as(usize, 0), VerifierFailureFollower.runs);
1032
1033 const failure = pm.getLastVerifierFailure() orelse return error.TestExpectedVerifierFailure;
1034 try testing.expectEqualStrings("append-invalid-constant", failure.pass_name);
1035 try testing.expectEqualStrings(test_dialect.TestDialect.ModuleOp.operation_name, failure.target_op_name);
1036 try testing.expectEqual(error.MissingRequiredAttribute, failure.err);
1037
1038 const reproducer = pm.getLastFailureReproducer() orelse return error.TestExpectedReproducer;
1039 try testing.expectEqualStrings("append-invalid-constant,must-not-run-after-verifier-failure", reproducer.pipeline);
1040 try testing.expect(reproducer.verifier_enabled);
1041 try testing.expectEqual(PassFailureKind.verifier, reproducer.failure_kind.?);
1042 try testing.expectEqualStrings("append-invalid-constant", reproducer.pass_name.?);
1043 try testing.expectEqual(error.MissingRequiredAttribute, reproducer.verifier_error.?);
1044 }
1045
1046 test "PassManager verifier is opt-in" {
1047 const testing = std.testing;
1048 const test_dialect = @import("../../dialects/fixture/root.zig");
1049
1050 var arena = alloc_arena.Arena.init(std.testing.allocator);
1051 defer arena.deinit();
1052 const allocator = arena.allocator();
1053
1054 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1055 defer ctx.deinit(allocator);
1056 try ir.dialects.loadDialectSpec(&ctx, test_dialect.TestDialect.spec);
1057
1058 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
1059
1060 var pm = PassManager.init(allocator);
1061 defer pm.deinit();
1062 try pm.addPass(.{
1063 .name = "append-invalid-constant",
1064 .description = "",
1065 .run_fn = appendInvalidTestConstant,
1066 });
1067
1068 try testing.expect(!pm.verifierEnabled());
1069 try testing.expectEqual(PassResult.success, pm.run(module_op.op, &ctx));
1070 try testing.expectEqual(@as(u64, 1), pm.stats.pass_runs);
1071 try testing.expectEqual(@as(u64, 1), ctx.passRunCount());
1072 try testing.expectEqual(@as(u64, 0), pm.stats.verifier_failures);
1073 try testing.expect(pm.getLastVerifierFailure() == null);
1074 }
1075
1076 const MissingDialectRunner = struct {
1077 var runs: usize = 0;
1078
1079 fn run(_: *PassContext) PassResult {
1080 runs += 1;
1081 return .success;
1082 }
1083 };
1084
1085 test "PassManager fails before running passes when dependent dialects are unavailable" {
1086 const testing = std.testing;
1087 const test_dialect = @import("../../dialects/fixture/root.zig");
1088
1089 var arena = alloc_arena.Arena.init(std.testing.allocator);
1090 defer arena.deinit();
1091 const allocator = arena.allocator();
1092
1093 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1094 defer ctx.deinit(allocator);
1095 try ir.dialects.loadDialectSpec(&ctx, test_dialect.TestDialect.spec);
1096
1097 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
1098 try ctx.requireRegistered();
1099
1100 MissingDialectRunner.runs = 0;
1101
1102 var pm = PassManager.init(allocator);
1103 defer pm.deinit();
1104 try pm.addPass(.{
1105 .name = "missing-dependent-dialect-runner",
1106 .description = "",
1107 .run_fn = MissingDialectRunner.run,
1108 .dependent_dialects = dialectDependencies(&.{"missingdep"}),
1109 });
1110
1111 try testing.expectEqual(PassResult.failure, pm.run(module_op.op, &ctx));
1112 try testing.expectEqual(@as(usize, 0), MissingDialectRunner.runs);
1113 try testing.expectEqual(@as(u64, 0), pm.stats.pass_runs);
1114 }
1115
1116 test "PassManager.runToFixedPoint stops on verifier failure" {
1117 const testing = std.testing;
1118 const test_dialect = @import("../../dialects/fixture/root.zig");
1119
1120 var arena = alloc_arena.Arena.init(std.testing.allocator);
1121 defer arena.deinit();
1122 const allocator = arena.allocator();
1123
1124 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1125 defer ctx.deinit(allocator);
1126 try ir.dialects.loadDialectSpec(&ctx, test_dialect.TestDialect.spec);
1127
1128 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
1129
1130 var pm = PassManager.init(allocator);
1131 defer pm.deinit();
1132 pm.enableVerifier();
1133 try pm.addPass(.{
1134 .name = "append-invalid-constant",
1135 .description = "",
1136 .run_fn = appendInvalidTestConstant,
1137 });
1138
1139 const result = pm.runToFixedPoint(module_op.op, &ctx, 8);
1140 try testing.expectEqual(PassResult.failure, result.result);
1141 try testing.expect(!result.changed);
1142 try testing.expectEqual(@as(usize, 1), result.iterations);
1143 try testing.expectEqual(@as(u64, 1), pm.stats.pass_runs);
1144 try testing.expectEqual(@as(u64, 1), ctx.passRunCount());
1145 try testing.expectEqual(@as(u64, 1), pm.stats.verifier_failures);
1146
1147 const failure = pm.getLastVerifierFailure() orelse return error.TestExpectedVerifierFailure;
1148 try testing.expectEqualStrings("append-invalid-constant", failure.pass_name);
1149 try testing.expectEqual(error.MissingRequiredAttribute, failure.err);
1150 }
1151
1152 const FixedPointDependencyRunner = struct {
1153 var runs: usize = 0;
1154
1155 fn run(ctx: *PassContext) PassResult {
1156 if (!ctx.ir_ctx.isDialectLoaded("fixeddep")) return .failure;
1157 runs += 1;
1158 ctx.preserveAllAnalyses();
1159 return .success;
1160 }
1161 };
1162
1163 test "PassManager.runToFixedPoint preloads dependent dialects before iteration" {
1164 const testing = std.testing;
1165 const test_dialect = @import("../../dialects/fixture/root.zig");
1166
1167 var arena = alloc_arena.Arena.init(std.testing.allocator);
1168 defer arena.deinit();
1169 const allocator = arena.allocator();
1170
1171 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1172 defer ctx.deinit(allocator);
1173 try ir.dialects.loadDialectSpec(&ctx, test_dialect.TestDialect.spec);
1174
1175 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
1176 try ctx.requireRegistered();
1177 try ctx.registerDialectLoader("fixeddep", loadFixedPointDependencyTestDialect);
1178
1179 FixedPointDependencyRunner.runs = 0;
1180
1181 var pm = PassManager.init(allocator);
1182 defer pm.deinit();
1183 try pm.addPass(.{
1184 .name = "fixed-dependent-dialect-runner",
1185 .description = "",
1186 .run_fn = FixedPointDependencyRunner.run,
1187 .dependent_dialects = dialectDependencies(&.{"fixeddep"}),
1188 });
1189
1190 const result = pm.runToFixedPoint(module_op.op, &ctx, 8);
1191 try testing.expectEqual(PassResult.success, result.result);
1192 try testing.expect(!result.changed);
1193 try testing.expectEqual(@as(usize, 1), result.iterations);
1194 try testing.expectEqual(@as(usize, 1), FixedPointDependencyRunner.runs);
1195 try testing.expect(ctx.isDialectLoaded("fixeddep"));
1196 }
1197
1198 const MutationScopeCapture = struct {
1199 scope: ?PassMutationScope = null,
1200
1201 fn before(raw: ?*anyopaque, info: PassInfo) void {
1202 const self: *@This() = @ptrCast(@alignCast(raw orelse return));
1203 self.scope = info.mutation_scope;
1204 }
1205 };
1206
1207 const MutationScopeRunner = struct {
1208 fn run(ctx: *PassContext) PassResult {
1209 ctx.preserveAllAnalyses();
1210 return .success;
1211 }
1212 };
1213
1214 const FixedPointRerunRunner = struct {
1215 var runs: usize = 0;
1216 var modify_first = false;
1217
1218 fn run(ctx: *PassContext) PassResult {
1219 runs += 1;
1220 if (modify_first and runs == 1) {
1221 ctx.markModified();
1222 } else {
1223 ctx.preserveAllAnalyses();
1224 }
1225 return .success;
1226 }
1227 };
1228
1229 const RerunIntervalRunner = struct {
1230 var runs: usize = 0;
1231
1232 fn preserve(ctx: *PassContext) PassResult {
1233 runs += 1;
1234 ctx.preserveAllAnalyses();
1235 return .success;
1236 }
1237
1238 fn modify(ctx: *PassContext) PassResult {
1239 runs += 1;
1240 ctx.markModified();
1241 return .success;
1242 }
1243 };
1244
1245 const StatefulRerunRunner = struct {
1246 fn run(_: ?*anyopaque, _: *PassContext) PassResult {
1247 return .success;
1248 }
1249 };
1250
1251 test "Pass mutation scope reaches instrumentation" {
1252 const testing = std.testing;
1253 const test_dialect = @import("../../dialects/fixture/root.zig");
1254
1255 var arena = alloc_arena.Arena.init(std.testing.allocator);
1256 defer arena.deinit();
1257 const allocator = arena.allocator();
1258
1259 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1260 defer ctx.deinit(allocator);
1261
1262 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
1263
1264 var capture = MutationScopeCapture{};
1265 var pm = PassManager.init(allocator);
1266 defer pm.deinit();
1267 try pm.addInstrumentation(.{
1268 .ctx = &capture,
1269 .runBeforePass = MutationScopeCapture.before,
1270 });
1271 try pm.addPass(.{
1272 .name = "read-only-test",
1273 .description = "records pass mutation metadata",
1274 .run_fn = MutationScopeRunner.run,
1275 .mutation_scope = .read_only,
1276 });
1277
1278 try testing.expectEqual(PassResult.success, pm.run(module_op.op, &ctx));
1279 try testing.expectEqual(PassMutationScope.read_only, capture.scope.?);
1280 }
1281
1282 test "PassManager skips a fixed-point rerun at the current revision" {
1283 const testing = std.testing;
1284 const test_dialect = @import("../../dialects/fixture/root.zig");
1285
1286 const allocator = testing.allocator;
1287
1288 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1289 defer ctx.deinit(allocator);
1290
1291 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
1292
1293 FixedPointRerunRunner.runs = 0;
1294 FixedPointRerunRunner.modify_first = true;
1295 RerunIntervalRunner.runs = 0;
1296
1297 const fixed_point_pass = Pass{
1298 .name = "fixed-point-rerun",
1299 .description = "",
1300 .run_fn = FixedPointRerunRunner.run,
1301 .rerun_policy = .skip_if_unchanged,
1302 };
1303
1304 var counter = CountingInstrumentation{};
1305 var pm = PassManager.init(allocator);
1306 defer pm.deinit();
1307 try pm.addInstrumentation(counter.instrumentation());
1308 try pm.addPass(fixed_point_pass);
1309 try pm.addPass(.{
1310 .name = "rerun-interval-preserver",
1311 .description = "",
1312 .run_fn = RerunIntervalRunner.preserve,
1313 .mutation_scope = .read_only,
1314 });
1315 try pm.addPass(fixed_point_pass);
1316
1317 try testing.expectEqual(PassResult.success, pm.run(module_op.op, &ctx));
1318 try testing.expectEqual(@as(usize, 1), FixedPointRerunRunner.runs);
1319 try testing.expectEqual(@as(usize, 1), RerunIntervalRunner.runs);
1320 try testing.expectEqual(@as(u64, 2), pm.stats.pass_runs);
1321 try testing.expectEqual(@as(u64, 1), pm.stats.passes_skipped);
1322 try testing.expectEqual(@as(u64, 1), pm.stats.passes_modified);
1323 try testing.expectEqual(@as(usize, 2), counter.pass_count);
1324 }
1325
1326 test "PassManager reruns a fixed-point pass after nested modification" {
1327 const testing = std.testing;
1328 const test_dialect = @import("../../dialects/fixture/root.zig");
1329
1330 const allocator = testing.allocator;
1331
1332 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1333 defer ctx.deinit(allocator);
1334 try ir.dialects.loadDialectSpec(&ctx, test_dialect.TestDialect.spec);
1335
1336 const loc = ir.Location.getUnknown();
1337 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
1338 const func_op = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "rerun_nested", &.{});
1339 try module_op.getBodyBlock().addOperation(func_op.op);
1340
1341 FixedPointRerunRunner.runs = 0;
1342 FixedPointRerunRunner.modify_first = false;
1343 RerunIntervalRunner.runs = 0;
1344
1345 const fixed_point_pass = Pass{
1346 .name = "fixed-point-rerun",
1347 .description = "",
1348 .run_fn = FixedPointRerunRunner.run,
1349 .rerun_policy = .skip_if_unchanged,
1350 };
1351
1352 var pm = PassManager.init(allocator);
1353 defer pm.deinit();
1354 try pm.addPass(fixed_point_pass);
1355 const nested = try pm.nest(test_dialect.TestDialect.FuncOp.operation_name);
1356 try nested.addPass(.{
1357 .name = "nested-rerun-revision",
1358 .description = "",
1359 .run_fn = RerunIntervalRunner.modify,
1360 });
1361 try pm.addPass(fixed_point_pass);
1362
1363 try testing.expectEqual(PassResult.success, pm.run(module_op.op, &ctx));
1364 try testing.expectEqual(@as(usize, 2), FixedPointRerunRunner.runs);
1365 try testing.expectEqual(@as(usize, 1), RerunIntervalRunner.runs);
1366 try testing.expectEqual(@as(u64, 3), pm.stats.pass_runs);
1367 try testing.expectEqual(@as(u64, 0), pm.stats.passes_skipped);
1368 try testing.expectEqual(@as(u64, 1), pm.stats.passes_modified);
1369 }
1370
1371 test "Pass rerun identity is stateless and configuration-specific" {
1372 const testing = std.testing;
1373
1374 const base = Pass{
1375 .name = "fixed-point-rerun",
1376 .description = "",
1377 .run_fn = FixedPointRerunRunner.run,
1378 .rerun_policy = .skip_if_unchanged,
1379 .textual_options = "mode=base",
1380 };
1381 var configured = base;
1382 configured.textual_options = "mode=other";
1383 var required = base;
1384 required.rerun_policy = .always;
1385
1386 try testing.expect(base.validRerunContract());
1387 try testing.expect(base.sameRerunIdentity(base));
1388 try testing.expect(!base.sameRerunIdentity(configured));
1389 try testing.expect(!base.sameRerunIdentity(required));
1390
1391 var state: u8 = 0;
1392 var pm = OpPassManager.init(testing.allocator, null);
1393 defer pm.deinit();
1394 try testing.expectError(error.InvalidPassRerunContract, pm.addPass(.{
1395 .name = "stateful-rerun",
1396 .description = "",
1397 .state = &state,
1398 .run_with_state_fn = StatefulRerunRunner.run,
1399 .rerun_policy = .skip_if_unchanged,
1400 }));
1401 }
1402
1403 const StatisticsRunner = struct {
1404 fn run(ctx: *PassContext) PassResult {
1405 ctx.incrementStatistic("folds", "folds applied");
1406 ctx.addStatistic("rewrites", "rewrites applied", 2);
1407 ctx.preserveAllAnalyses();
1408 return .success;
1409 }
1410 };
1411
1412 test "PassContext reports pass statistics through instrumentation" {
1413 const testing = std.testing;
1414 const test_dialect = @import("../../dialects/fixture/root.zig");
1415
1416 const allocator = testing.allocator;
1417
1418 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1419 defer ctx.deinit(allocator);
1420
1421 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
1422
1423 var statistics = PassStatisticsInstrumentation.init(allocator);
1424 defer statistics.deinit();
1425
1426 var pm = PassManager.init(allocator);
1427 defer pm.deinit();
1428 try pm.addInstrumentation(statistics.instrumentation());
1429 try pm.addPass(.{
1430 .name = "stat-pass",
1431 .description = "reports pass statistics",
1432 .run_fn = StatisticsRunner.run,
1433 .mutation_scope = .read_only,
1434 });
1435
1436 try testing.expectEqual(PassResult.success, pm.run(module_op.op, &ctx));
1437 try testing.expectEqual(@as(u64, 1), statistics.get("stat-pass", "folds").?);
1438 try testing.expectEqual(@as(u64, 2), statistics.get("stat-pass", "rewrites").?);
1439
1440 const summaries = try statistics.summariesAlloc(allocator);
1441 defer allocator.free(summaries);
1442 try testing.expectEqual(@as(usize, 2), summaries.len);
1443 try testing.expectEqualStrings("stat-pass", summaries[0].pass_name);
1444 try testing.expectEqualStrings("folds", summaries[0].name);
1445 try testing.expectEqualStrings("folds applied", summaries[0].description);
1446 try testing.expectEqual(@as(u64, 1), summaries[0].value);
1447 try testing.expectEqualStrings("rewrites", summaries[1].name);
1448 try testing.expectEqual(@as(u64, 2), summaries[1].value);
1449 }
1450
1451 const DuplicateStatisticsRunner = struct {
1452 fn run(ctx: *PassContext) PassResult {
1453 ctx.incrementStatistic("matches", "matched operations");
1454 ctx.preserveAllAnalyses();
1455 return .success;
1456 }
1457 };
1458
1459 test "Pass statistics aggregate duplicate pass names" {
1460 const testing = std.testing;
1461 const test_dialect = @import("../../dialects/fixture/root.zig");
1462
1463 const allocator = testing.allocator;
1464
1465 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1466 defer ctx.deinit(allocator);
1467
1468 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
1469
1470 var statistics = PassStatisticsInstrumentation.init(allocator);
1471 defer statistics.deinit();
1472
1473 var pm = PassManager.init(allocator);
1474 defer pm.deinit();
1475 try pm.addInstrumentation(statistics.instrumentation());
1476 try pm.addPass(.{
1477 .name = "repeat-pass",
1478 .description = "reports pass statistics",
1479 .run_fn = DuplicateStatisticsRunner.run,
1480 .mutation_scope = .read_only,
1481 });
1482 try pm.addPass(.{
1483 .name = "repeat-pass",
1484 .description = "reports pass statistics again",
1485 .run_fn = DuplicateStatisticsRunner.run,
1486 .mutation_scope = .read_only,
1487 });
1488
1489 try testing.expectEqual(PassResult.success, pm.run(module_op.op, &ctx));
1490 try testing.expectEqual(@as(u64, 2), statistics.get("repeat-pass", "matches").?);
1491
1492 const summaries = try statistics.summariesAlloc(allocator);
1493 defer allocator.free(summaries);
1494 try testing.expectEqual(@as(usize, 1), summaries.len);
1495 try testing.expectEqualStrings("repeat-pass", summaries[0].pass_name);
1496 try testing.expectEqualStrings("matches", summaries[0].name);
1497 try testing.expectEqual(@as(u64, 2), summaries[0].value);
1498 }
1499
1500 const ConfiguredPassState = struct {
1501 runs: usize = 0,
1502 };
1503
1504 const ConfiguredPassRunner = struct {
1505 fn run(raw: ?*anyopaque, ctx: *PassContext) PassResult {
1506 const state: *ConfiguredPassState = @ptrCast(@alignCast(raw orelse return .failure));
1507 state.runs += 1;
1508 ctx.preserveAllAnalyses();
1509 return .success;
1510 }
1511 };
1512
1513 test "Pass state reaches configured run function" {
1514 const testing = std.testing;
1515 const test_dialect = @import("../../dialects/fixture/root.zig");
1516
1517 var arena = alloc_arena.Arena.init(std.testing.allocator);
1518 defer arena.deinit();
1519 const allocator = arena.allocator();
1520
1521 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1522 defer ctx.deinit(allocator);
1523
1524 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
1525
1526 var state = ConfiguredPassState{};
1527 var pm = PassManager.init(allocator);
1528 defer pm.deinit();
1529 try pm.addPass(.{
1530 .name = "stateful-test",
1531 .description = "records configured pass state",
1532 .state = &state,
1533 .run_with_state_fn = ConfiguredPassRunner.run,
1534 .mutation_scope = .read_only,
1535 });
1536
1537 try testing.expectEqual(PassResult.success, pm.run(module_op.op, &ctx));
1538 try testing.expectEqual(@as(usize, 1), state.runs);
1539 }
1540
1541 const StableFixedPointPass = struct {
1542 var runs: usize = 0;
1543 var changed_runs: usize = 0;
1544
1545 fn run(ctx: *PassContext) PassResult {
1546 runs += 1;
1547 if (changed_runs < 2) {
1548 changed_runs += 1;
1549 ctx.markModified();
1550 } else {
1551 ctx.preserveAllAnalyses();
1552 }
1553 return .success;
1554 }
1555 };
1556
1557 test "PassManager.runToFixedPoint stops after stable iteration" {
1558 const testing = std.testing;
1559 const test_dialect = @import("../../dialects/fixture/root.zig");
1560
1561 var arena = alloc_arena.Arena.init(std.testing.allocator);
1562 defer arena.deinit();
1563 const allocator = arena.allocator();
1564
1565 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1566 defer ctx.deinit(allocator);
1567
1568 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
1569
1570 StableFixedPointPass.runs = 0;
1571 StableFixedPointPass.changed_runs = 0;
1572
1573 var pm = PassManager.init(allocator);
1574 defer pm.deinit();
1575 try pm.addPass(.{
1576 .name = "fixed-point-test",
1577 .description = "changes twice before stabilizing",
1578 .run_fn = StableFixedPointPass.run,
1579 });
1580
1581 const result = pm.runToFixedPoint(module_op.op, &ctx, 8);
1582 try testing.expectEqual(PassResult.success, result.result);
1583 try testing.expect(result.changed);
1584 try testing.expectEqual(@as(usize, 3), result.iterations);
1585 try testing.expectEqual(@as(usize, 3), StableFixedPointPass.runs);
1586 try testing.expectEqual(@as(usize, 2), StableFixedPointPass.changed_runs);
1587 try testing.expectEqual(@as(u64, 3), pm.stats.pass_runs);
1588 try testing.expectEqual(@as(u64, 2), pm.stats.passes_modified);
1589 }
1590
1591 test "PassManager.runToFixedPoint reports failure" {
1592 const testing = std.testing;
1593 const test_dialect = @import("../../dialects/fixture/root.zig");
1594
1595 var arena = alloc_arena.Arena.init(std.testing.allocator);
1596 defer arena.deinit();
1597 const allocator = arena.allocator();
1598
1599 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1600 defer ctx.deinit(allocator);
1601
1602 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
1603
1604 var pm = PassManager.init(allocator);
1605 defer pm.deinit();
1606 try pm.addPass(.{
1607 .name = "fixed-point-failure",
1608 .description = "fails fixed-point execution",
1609 .run_fn = test_pass_failure,
1610 });
1611
1612 const result = pm.runToFixedPoint(module_op.op, &ctx, 8);
1613 try testing.expectEqual(PassResult.failure, result.result);
1614 try testing.expect(!result.changed);
1615 try testing.expectEqual(@as(usize, 1), result.iterations);
1616 try testing.expectEqual(@as(u64, 1), pm.stats.pass_runs);
1617 try testing.expectEqual(@as(u64, 1), pm.stats.pass_failures);
1618 }
1619
1620 const HierarchicalFuncCounter = struct {
1621 var count: usize = 0;
1622
1623 fn run(ctx: *PassContext) PassResult {
1624 if (std.mem.eql(u8, ctx.op.name.name, "test.func")) {
1625 count += 1;
1626 }
1627 return .success;
1628 }
1629 };
1630
1631 test "Hierarchical pass manager runs only on matching ops" {
1632 const testing = std.testing;
1633 const test_dialect = @import("../../dialects/fixture/root.zig");
1634
1635 var arena = alloc_arena.Arena.init(std.testing.allocator);
1636 defer arena.deinit();
1637 const allocator = arena.allocator();
1638
1639 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1640 defer ctx.deinit(allocator);
1641 try ctx.allowUnregistered();
1642
1643 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
1644 const module_block = module_op.getBodyBlock();
1645
1646 var builder = ir.OperationBuilder.init(&ctx);
1647 const target1_state = ir.Operation.State.init("test.target_op", ir.Location.getUnknown());
1648 const target1_op = try builder.create(target1_state);
1649 try module_block.addOperation(target1_op);
1650
1651 const func_op = try test_dialect.TestDialect.FuncOp.create(&ctx, ir.Location.getUnknown(), "nested_func", &.{});
1652 try module_block.addOperation(func_op.op);
1653
1654 const func_block = func_op.getEntryBlock();
1655 const target2_state = ir.Operation.State.init("test.target_op", ir.Location.getUnknown());
1656 const target2_op = try builder.create(target2_state);
1657 try func_block.addOperation(target2_op);
1658
1659 var pm = PassManager.init(allocator);
1660 defer pm.deinit();
1661
1662 HierarchicalFuncCounter.count = 0;
1663
1664 const func_pm = try pm.nest("test.func");
1665 try func_pm.addPass(Pass{
1666 .name = "func-counter",
1667 .description = "Counts func ops",
1668 .run_fn = HierarchicalFuncCounter.run,
1669 });
1670
1671 const result = pm.run(module_op.op, &ctx);
1672 try testing.expectEqual(PassResult.success, result);
1673 try testing.expectEqual(@as(usize, 1), HierarchicalFuncCounter.count);
1674 }
1675
1676 const DeepBinaryCounter = struct {
1677 var count: usize = 0;
1678
1679 fn run(ctx: *PassContext) PassResult {
1680 if (std.mem.eql(u8, ctx.op.name.name, "test.binary")) {
1681 count += 1;
1682 }
1683 return .success;
1684 }
1685 };
1686
1687 test "Hierarchical pass manager rejects non-isolated deeply nested targets" {
1688 const testing = std.testing;
1689 const test_dialect = @import("../../dialects/fixture/root.zig");
1690
1691 var arena = alloc_arena.Arena.init(std.testing.allocator);
1692 defer arena.deinit();
1693 const allocator = arena.allocator();
1694
1695 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1696 defer ctx.deinit(allocator);
1697
1698 const loc = ir.Location.getUnknown();
1699 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
1700 const module_block = module_op.getBodyBlock();
1701
1702 const outer_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "outer", &.{});
1703 try module_block.addOperation(outer_func.op);
1704
1705 const inner_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "inner", &.{});
1706 try outer_func.getEntryBlock().addOperation(inner_func.op);
1707
1708 const i32_type = try test_dialect.TestDialect.getI32Type(&ctx);
1709 _ = try inner_func.getEntryBlock().addArgument(i32_type, loc);
1710 _ = try inner_func.getEntryBlock().addArgument(i32_type, loc);
1711 const inner_args = inner_func.getEntryBlock().arguments.items;
1712 const binary_op = try test_dialect.TestDialect.BinaryOp.create(&ctx, loc, inner_args[0], inner_args[1]);
1713 try inner_func.getEntryBlock().addOperation(binary_op.op);
1714
1715 var pm = PassManager.init(allocator);
1716 defer pm.deinit();
1717
1718 DeepBinaryCounter.count = 0;
1719
1720 const func_pm = try pm.nest("test.func");
1721 const binary_pm = try func_pm.nest("test.binary");
1722 try binary_pm.addPass(Pass{
1723 .name = "binary-counter",
1724 .description = "Counts binary ops",
1725 .run_fn = DeepBinaryCounter.run,
1726 });
1727
1728 const result = pm.run(module_op.op, &ctx);
1729 try testing.expectEqual(PassResult.failure, result);
1730 try testing.expectEqual(@as(usize, 0), DeepBinaryCounter.count);
1731 }
1732
1733 test "Hierarchical pass manager propagates failure" {
1734 const testing = std.testing;
1735 const test_dialect = @import("../../dialects/fixture/root.zig");
1736
1737 var arena = alloc_arena.Arena.init(std.testing.allocator);
1738 defer arena.deinit();
1739 const allocator = arena.allocator();
1740
1741 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1742 defer ctx.deinit(allocator);
1743
1744 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
1745 const module_block = module_op.getBodyBlock();
1746
1747 const func_op = try test_dialect.TestDialect.FuncOp.create(&ctx, ir.Location.getUnknown(), "fail_func", &.{});
1748 try module_block.addOperation(func_op.op);
1749
1750 var pm = PassManager.init(allocator);
1751 defer pm.deinit();
1752
1753 const func_pm = try pm.nest("test.func");
1754 try func_pm.addPass(Pass{
1755 .name = "failing-pass",
1756 .description = "Always fails",
1757 .run_fn = test_pass_failure,
1758 });
1759
1760 const result = pm.run(module_op.op, &ctx);
1761 try testing.expectEqual(PassResult.failure, result);
1762 try testing.expectEqual(@as(u64, 1), pm.stats.pass_failures);
1763 }
1764
1765 test "PassManager captures serial pass failure reproducer" {
1766 const testing = std.testing;
1767 const test_dialect = @import("../../dialects/fixture/root.zig");
1768
1769 var arena = alloc_arena.Arena.init(std.testing.allocator);
1770 defer arena.deinit();
1771 const allocator = arena.allocator();
1772
1773 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1774 defer ctx.deinit(allocator);
1775
1776 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
1777
1778 var pm = PassManager.init(allocator);
1779 defer pm.deinit();
1780
1781 try pm.addPass(Pass{
1782 .name = "serial-reproducer-fail",
1783 .description = "fails for reproducer capture",
1784 .run_fn = test_pass_failure,
1785 });
1786
1787 const result = pm.run(module_op.op, &ctx);
1788 try testing.expectEqual(PassResult.failure, result);
1789
1790 const reproducer = pm.getLastFailureReproducer() orelse return error.TestExpectedReproducer;
1791 try testing.expectEqualStrings("serial-reproducer-fail", reproducer.pipeline);
1792 try testing.expectEqual(@as(usize, 1), reproducer.max_threads);
1793 try testing.expectEqual(@as(usize, 1), reproducer.worker_count);
1794 try testing.expect(!reproducer.verifier_enabled);
1795 try testing.expectEqual(PassFailureKind.pass, reproducer.failure_kind.?);
1796 try testing.expectEqualStrings("serial-reproducer-fail", reproducer.pass_name.?);
1797 try testing.expectEqualStrings(module_op.op.name.name, reproducer.target_op_name.?);
1798 try testing.expect(std.mem.indexOf(u8, reproducer.ir, module_op.op.name.name) != null);
1799 }
1800
1801 const NestedPassOrderTracker = struct {
1802 var order: [3]u8 = undefined;
1803 var idx: usize = 0;
1804
1805 fn pass1(_: *PassContext) PassResult {
1806 order[idx] = '1';
1807 idx += 1;
1808 return .success;
1809 }
1810
1811 fn pass2(_: *PassContext) PassResult {
1812 order[idx] = '2';
1813 idx += 1;
1814 return .success;
1815 }
1816
1817 fn pass3(_: *PassContext) PassResult {
1818 order[idx] = '3';
1819 idx += 1;
1820 return .success;
1821 }
1822 };
1823
1824 test "Multiple passes in nested manager run sequentially" {
1825 const testing = std.testing;
1826 const test_dialect = @import("../../dialects/fixture/root.zig");
1827
1828 var arena = alloc_arena.Arena.init(std.testing.allocator);
1829 defer arena.deinit();
1830 const allocator = arena.allocator();
1831
1832 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1833 defer ctx.deinit(allocator);
1834
1835 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
1836 const module_block = module_op.getBodyBlock();
1837
1838 const func_op = try test_dialect.TestDialect.FuncOp.create(&ctx, ir.Location.getUnknown(), "multi_pass", &.{});
1839 try module_block.addOperation(func_op.op);
1840
1841 var pm = PassManager.init(allocator);
1842 defer pm.deinit();
1843
1844 NestedPassOrderTracker.idx = 0;
1845
1846 const func_pm = try pm.nest("test.func");
1847 try func_pm.addPass(Pass{
1848 .name = "pass1",
1849 .description = "",
1850 .run_fn = NestedPassOrderTracker.pass1,
1851 });
1852 try func_pm.addPass(Pass{
1853 .name = "pass2",
1854 .description = "",
1855 .run_fn = NestedPassOrderTracker.pass2,
1856 });
1857 try func_pm.addPass(Pass{
1858 .name = "pass3",
1859 .description = "",
1860 .run_fn = NestedPassOrderTracker.pass3,
1861 });
1862
1863 const result = pm.run(module_op.op, &ctx);
1864 try testing.expectEqual(PassResult.success, result);
1865 try testing.expectEqual(@as(usize, 3), NestedPassOrderTracker.idx);
1866 try testing.expectEqualStrings("123", &NestedPassOrderTracker.order);
1867 }
1868
1869 const ReadOnlyParallelBarrierPass = struct {
1870 var started = std.atomic.Value(usize).init(0);
1871 var release = std.atomic.Value(bool).init(false);
1872
1873 fn run(ctx: *PassContext) PassResult {
1874 if (ctx.workerCount(8) != 2) return .failure;
1875 if (!ctx.ir_ctx.isMultithreadedExecution()) return .failure;
1876 ctx.preserveAllAnalyses();
1877 const arrived = started.fetchAdd(1, .acq_rel) + 1;
1878 if (arrived == 2) release.store(true, .release);
1879
1880 var spins: usize = 0;
1881 while (!release.load(.acquire)) {
1882 spins += 1;
1883 if (spins > 1_000_000) return .failure;
1884 sys.thread.yield();
1885 }
1886 return .success;
1887 }
1888 };
1889
1890 test "PassManager.runWithOptions runs read-only nested targets concurrently" {
1891 const testing = std.testing;
1892 const test_dialect = @import("../../dialects/fixture/root.zig");
1893
1894 var arena = alloc_arena.Arena.init(std.testing.allocator);
1895 defer arena.deinit();
1896 const allocator = arena.allocator();
1897
1898 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1899 defer ctx.deinit(allocator);
1900
1901 const loc = ir.Location.getUnknown();
1902 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
1903 const module_block = module_op.getBodyBlock();
1904
1905 const first_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "first", &.{});
1906 try module_block.addOperation(first_func.op);
1907 const second_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "second", &.{});
1908 try module_block.addOperation(second_func.op);
1909
1910 ReadOnlyParallelBarrierPass.started.store(0, .release);
1911 ReadOnlyParallelBarrierPass.release.store(false, .release);
1912
1913 var pm = PassManager.init(allocator);
1914 defer pm.deinit();
1915
1916 const func_pm = try pm.nest("test.func");
1917 try func_pm.addPass(Pass{
1918 .name = "barrier-read-only",
1919 .description = "waits for another read-only target",
1920 .run_fn = ReadOnlyParallelBarrierPass.run,
1921 .mutation_scope = .read_only,
1922 });
1923
1924 const result = pm.runWithOptions(module_op.op, &ctx, .{ .max_threads = 2 });
1925 try testing.expectEqual(PassResult.success, result);
1926 try testing.expectEqual(@as(usize, 2), ReadOnlyParallelBarrierPass.started.load(.acquire));
1927 try testing.expectEqual(@as(u64, 2), pm.stats.pass_runs);
1928 try testing.expectEqual(@as(u64, 0), pm.stats.pass_failures);
1929 }
1930
1931 const ParallelContextStoragePass = struct {
1932 var attr_value: i64 = 0;
1933
1934 fn run(ctx: *PassContext) PassResult {
1935 if (!ctx.ir_ctx.isMultithreadedExecution()) return .failure;
1936 _ = ctx.ir_ctx.getI64Attr(attr_value) catch return .failure;
1937 ctx.preserveAllAnalyses();
1938 return .success;
1939 }
1940 };
1941
1942 test "PassManager.runWithOptions blocks context storage misses in parallel read-only passes" {
1943 const testing = std.testing;
1944 const test_dialect = @import("../../dialects/fixture/root.zig");
1945
1946 var arena = alloc_arena.Arena.init(std.testing.allocator);
1947 defer arena.deinit();
1948 const allocator = arena.allocator();
1949
1950 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1951 defer ctx.deinit(allocator);
1952
1953 const loc = ir.Location.getUnknown();
1954 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
1955 const module_block = module_op.getBodyBlock();
1956
1957 const first_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "first", &.{});
1958 try module_block.addOperation(first_func.op);
1959 const second_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "second", &.{});
1960 try module_block.addOperation(second_func.op);
1961
1962 _ = try ctx.getI64Attr(7);
1963 ParallelContextStoragePass.attr_value = 7;
1964
1965 var preload_pm = PassManager.init(allocator);
1966 defer preload_pm.deinit();
1967 const preload_func_pm = try preload_pm.nest("test.func");
1968 try preload_func_pm.addPass(Pass{
1969 .name = "preloaded-context-storage",
1970 .description = "reads preloaded context storage",
1971 .run_fn = ParallelContextStoragePass.run,
1972 .mutation_scope = .read_only,
1973 });
1974
1975 const preload_result = preload_pm.runWithOptions(module_op.op, &ctx, .{ .max_threads = 2 });
1976 try testing.expectEqual(PassResult.success, preload_result);
1977 try testing.expectEqual(@as(u64, 2), preload_pm.stats.pass_runs);
1978
1979 ParallelContextStoragePass.attr_value = 8;
1980
1981 var miss_pm = PassManager.init(allocator);
1982 defer miss_pm.deinit();
1983 const miss_func_pm = try miss_pm.nest("test.func");
1984 try miss_func_pm.addPass(Pass{
1985 .name = "missing-context-storage",
1986 .description = "attempts context storage creation",
1987 .run_fn = ParallelContextStoragePass.run,
1988 .mutation_scope = .read_only,
1989 });
1990
1991 const miss_result = miss_pm.runWithOptions(module_op.op, &ctx, .{ .max_threads = 2 });
1992 try testing.expectEqual(PassResult.failure, miss_result);
1993 try testing.expectEqual(@as(u64, 2), miss_pm.stats.pass_runs);
1994 try testing.expectEqual(@as(u64, 2), miss_pm.stats.pass_failures);
1995 }
1996
1997 const ParallelReproducerFailingPass = struct {
1998 fn run(ctx: *PassContext) PassResult {
1999 if (!ctx.ir_ctx.isMultithreadedExecution()) return .failure;
2000 return .failure;
2001 }
2002 };
2003
2004 test "PassManager.runWithOptions captures parallel pass failure reproducer in target order" {
2005 const testing = std.testing;
2006 const test_dialect = @import("../../dialects/fixture/root.zig");
2007
2008 var arena = alloc_arena.Arena.init(std.testing.allocator);
2009 defer arena.deinit();
2010 const allocator = arena.allocator();
2011
2012 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2013 defer ctx.deinit(allocator);
2014
2015 const loc = ir.Location.getUnknown();
2016 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
2017 const module_block = module_op.getBodyBlock();
2018
2019 const first_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "first", &.{});
2020 try module_block.addOperation(first_func.op);
2021 const second_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "second", &.{});
2022 try module_block.addOperation(second_func.op);
2023
2024 var pm = PassManager.init(allocator);
2025 defer pm.deinit();
2026
2027 const func_pm = try pm.nest("test.func");
2028 try func_pm.addPass(Pass{
2029 .name = "parallel-reproducer-fail",
2030 .description = "fails on each read-only target",
2031 .run_fn = ParallelReproducerFailingPass.run,
2032 .mutation_scope = .read_only,
2033 });
2034
2035 const result = pm.runWithOptions(module_op.op, &ctx, .{ .max_threads = 2 });
2036 try testing.expectEqual(PassResult.failure, result);
2037
2038 const reproducer = pm.getLastFailureReproducer() orelse return error.TestExpectedReproducer;
2039 try testing.expectEqualStrings("test.func(parallel-reproducer-fail)", reproducer.pipeline);
2040 try testing.expectEqual(@as(usize, 2), reproducer.max_threads);
2041 try testing.expectEqual(@as(usize, 2), reproducer.worker_count);
2042 try testing.expectEqual(PassFailureKind.pass, reproducer.failure_kind.?);
2043 try testing.expectEqualStrings("parallel-reproducer-fail", reproducer.pass_name.?);
2044 try testing.expectEqualStrings("test.func", reproducer.target_op_name.?);
2045 try testing.expectEqualStrings("first", reproducer.target_symbol_name.?);
2046 try testing.expect(std.mem.indexOf(u8, reproducer.ir, "second") != null);
2047 }
2048
2049 const ParallelDiagnosticRecorder = struct {
2050 messages: [2][]const u8 = .{ "", "" },
2051 seen: usize = 0,
2052
2053 fn handle(
2054 context: ?*anyopaque,
2055 diagnostic: *const diagnostics.Diagnostic,
2056 ) !diagnostics.HandlerResult {
2057 const self: *@This() = @ptrCast(@alignCast(context.?));
2058 self.messages[self.seen] = diagnostic.message;
2059 self.seen += 1;
2060 return .consumed;
2061 }
2062 };
2063
2064 const ParallelDiagnosticPass = struct {
2065 var second_emitted = std.atomic.Value(bool).init(false);
2066
2067 fn run(ctx: *PassContext) PassResult {
2068 const symbol_name = ir.SymbolTable.getSymbolName(ctx.op) orelse return .failure;
2069 const is_first = std.mem.eql(u8, symbol_name, "first");
2070 if (is_first) {
2071 var spins: usize = 0;
2072 while (!second_emitted.load(.acquire)) {
2073 spins += 1;
2074 if (spins > 1_000_000) return .failure;
2075 sys.thread.yield();
2076 }
2077 }
2078
2079 const message: []const u8 = if (is_first) "first diagnostic" else "second diagnostic";
2080 var diagnostic = ctx.op.emitWarning(message);
2081 defer diagnostic.deinit();
2082 _ = diagnostic.emit() catch return .failure;
2083
2084 if (!is_first) second_emitted.store(true, .release);
2085 ctx.preserveAllAnalyses();
2086 return .success;
2087 }
2088 };
2089
2090 test "PassManager.runWithOptions replays parallel diagnostics in target order" {
2091 const testing = std.testing;
2092 const test_dialect = @import("../../dialects/fixture/root.zig");
2093
2094 var arena = alloc_arena.Arena.init(std.testing.allocator);
2095 defer arena.deinit();
2096 const allocator = arena.allocator();
2097
2098 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2099 defer ctx.deinit(allocator);
2100
2101 const loc = ir.Location.getUnknown();
2102 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
2103 const module_block = module_op.getBodyBlock();
2104
2105 const first_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "first", &.{});
2106 try module_block.addOperation(first_func.op);
2107 const second_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "second", &.{});
2108 try module_block.addOperation(second_func.op);
2109
2110 var recorder = ParallelDiagnosticRecorder{};
2111 _ = try ctx.registerDiagnosticHandler(.{
2112 .context = &recorder,
2113 .handle = ParallelDiagnosticRecorder.handle,
2114 });
2115 ParallelDiagnosticPass.second_emitted.store(false, .release);
2116
2117 var pm = PassManager.init(allocator);
2118 defer pm.deinit();
2119
2120 const func_pm = try pm.nest("test.func");
2121 try func_pm.addPass(Pass{
2122 .name = "diagnostic-order-check",
2123 .description = "emits diagnostics in scheduling-inverted order",
2124 .run_fn = ParallelDiagnosticPass.run,
2125 .mutation_scope = .read_only,
2126 });
2127
2128 const result = pm.runWithOptions(module_op.op, &ctx, .{ .max_threads = 2 });
2129 try testing.expectEqual(PassResult.success, result);
2130 try testing.expectEqual(@as(usize, 2), recorder.seen);
2131 try testing.expectEqualStrings("first diagnostic", recorder.messages[0]);
2132 try testing.expectEqualStrings("second diagnostic", recorder.messages[1]);
2133 }
2134
2135 const VerifierParallelBarrierPass = struct {
2136 var started = std.atomic.Value(usize).init(0);
2137 var release = std.atomic.Value(bool).init(false);
2138
2139 fn run(ctx: *PassContext) PassResult {
2140 if (!ctx.ir_ctx.isMultithreadedExecution()) return .failure;
2141 ctx.preserveAllAnalyses();
2142 const arrived = started.fetchAdd(1, .acq_rel) + 1;
2143 if (arrived == 2) release.store(true, .release);
2144
2145 var spins: usize = 0;
2146 while (!release.load(.acquire)) {
2147 spins += 1;
2148 if (spins > 1_000_000) return .failure;
2149 sys.thread.yield();
2150 }
2151 return .success;
2152 }
2153 };
2154
2155 test "PassManager.runWithOptions keeps read-only targets parallel with verifier enabled" {
2156 const testing = std.testing;
2157 const test_dialect = @import("../../dialects/fixture/root.zig");
2158
2159 var arena = alloc_arena.Arena.init(std.testing.allocator);
2160 defer arena.deinit();
2161 const allocator = arena.allocator();
2162
2163 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2164 defer ctx.deinit(allocator);
2165 try ir.dialects.loadDialectSpec(&ctx, test_dialect.TestDialect.spec);
2166
2167 const loc = ir.Location.getUnknown();
2168 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
2169 const module_block = module_op.getBodyBlock();
2170
2171 const first_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "first", &.{});
2172 try module_block.addOperation(first_func.op);
2173 const second_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "second", &.{});
2174 try module_block.addOperation(second_func.op);
2175
2176 VerifierParallelBarrierPass.started.store(0, .release);
2177 VerifierParallelBarrierPass.release.store(false, .release);
2178
2179 var pm = PassManager.init(allocator);
2180 defer pm.deinit();
2181 pm.enableVerifier();
2182
2183 const func_pm = try pm.nest("test.func");
2184 try func_pm.addPass(Pass{
2185 .name = "verifier-parallel-barrier",
2186 .description = "waits for another verified read-only target",
2187 .run_fn = VerifierParallelBarrierPass.run,
2188 .mutation_scope = .read_only,
2189 });
2190
2191 const result = pm.runWithOptions(module_op.op, &ctx, .{ .max_threads = 2 });
2192 try testing.expectEqual(PassResult.success, result);
2193 try testing.expectEqual(@as(usize, 2), VerifierParallelBarrierPass.started.load(.acquire));
2194 try testing.expectEqual(@as(u64, 2), pm.stats.pass_runs);
2195 try testing.expectEqual(@as(u64, 0), pm.stats.verifier_failures);
2196 try testing.expect(pm.getLastVerifierFailure() == null);
2197 }
2198
2199 const ExclusivePassState = struct {
2200 runs: usize = 0,
2201 };
2202
2203 const ExclusiveStateSerialPass = struct {
2204 fn run(raw: ?*anyopaque, ctx: *PassContext) PassResult {
2205 if (ctx.ir_ctx.isMultithreadedExecution()) return .failure;
2206 const state: *ExclusivePassState = @ptrCast(@alignCast(raw orelse return .failure));
2207 state.runs += 1;
2208 ctx.preserveAllAnalyses();
2209 return .success;
2210 }
2211 };
2212
2213 test "PassManager.runWithOptions keeps exclusive stateful read-only passes serial" {
2214 const testing = std.testing;
2215 const test_dialect = @import("../../dialects/fixture/root.zig");
2216
2217 var arena = alloc_arena.Arena.init(std.testing.allocator);
2218 defer arena.deinit();
2219 const allocator = arena.allocator();
2220
2221 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2222 defer ctx.deinit(allocator);
2223
2224 const loc = ir.Location.getUnknown();
2225 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
2226 const module_block = module_op.getBodyBlock();
2227
2228 const first_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "first", &.{});
2229 try module_block.addOperation(first_func.op);
2230 const second_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "second", &.{});
2231 try module_block.addOperation(second_func.op);
2232
2233 var state = ExclusivePassState{};
2234 var pm = PassManager.init(allocator);
2235 defer pm.deinit();
2236
2237 const func_pm = try pm.nest("test.func");
2238 try func_pm.addPass(Pass{
2239 .name = "exclusive-state-serial-check",
2240 .description = "records serial exclusive state",
2241 .state = &state,
2242 .run_with_state_fn = ExclusiveStateSerialPass.run,
2243 .mutation_scope = .read_only,
2244 });
2245
2246 const result = pm.runWithOptions(module_op.op, &ctx, .{ .max_threads = 2 });
2247 try testing.expectEqual(PassResult.success, result);
2248 try testing.expectEqual(@as(usize, 2), state.runs);
2249 }
2250
2251 const SharedPassState = struct {
2252 started: std.atomic.Value(usize) = std.atomic.Value(usize).init(0),
2253 release: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
2254 };
2255
2256 const SharedStateParallelPass = struct {
2257 fn run(raw: ?*anyopaque, ctx: *PassContext) PassResult {
2258 if (!ctx.ir_ctx.isMultithreadedExecution()) return .failure;
2259 const state: *SharedPassState = @ptrCast(@alignCast(raw orelse return .failure));
2260 const arrived = state.started.fetchAdd(1, .acq_rel) + 1;
2261 if (arrived == 2) state.release.store(true, .release);
2262
2263 var spins: usize = 0;
2264 while (!state.release.load(.acquire)) {
2265 spins += 1;
2266 if (spins > 1_000_000) return .failure;
2267 sys.thread.yield();
2268 }
2269 ctx.preserveAllAnalyses();
2270 return .success;
2271 }
2272 };
2273
2274 test "PassManager.runWithOptions runs shared stateful read-only passes in parallel" {
2275 const testing = std.testing;
2276 const test_dialect = @import("../../dialects/fixture/root.zig");
2277
2278 var arena = alloc_arena.Arena.init(std.testing.allocator);
2279 defer arena.deinit();
2280 const allocator = arena.allocator();
2281
2282 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2283 defer ctx.deinit(allocator);
2284
2285 const loc = ir.Location.getUnknown();
2286 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
2287 const module_block = module_op.getBodyBlock();
2288
2289 const first_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "first", &.{});
2290 try module_block.addOperation(first_func.op);
2291 const second_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "second", &.{});
2292 try module_block.addOperation(second_func.op);
2293
2294 var state = SharedPassState{};
2295 var pm = PassManager.init(allocator);
2296 defer pm.deinit();
2297
2298 const func_pm = try pm.nest("test.func");
2299 try func_pm.addPass(Pass{
2300 .name = "shared-state-parallel-check",
2301 .description = "coordinates through explicit shared state",
2302 .state = &state,
2303 .run_with_state_fn = SharedStateParallelPass.run,
2304 .state_concurrency = .shared,
2305 .mutation_scope = .read_only,
2306 });
2307
2308 const result = pm.runWithOptions(module_op.op, &ctx, .{ .max_threads = 2 });
2309 try testing.expectEqual(PassResult.success, result);
2310 try testing.expectEqual(@as(usize, 2), state.started.load(.acquire));
2311 }
2312
2313 const ClonedPassStats = struct {
2314 clones: std.atomic.Value(usize) = std.atomic.Value(usize).init(0),
2315 deinit_count: std.atomic.Value(usize) = std.atomic.Value(usize).init(0),
2316 runs: std.atomic.Value(usize) = std.atomic.Value(usize).init(0),
2317 started: std.atomic.Value(usize) = std.atomic.Value(usize).init(0),
2318 release: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
2319 };
2320
2321 const ClonedPassState = struct {
2322 stats: *ClonedPassStats,
2323 original: bool,
2324 runs: usize = 0,
2325 };
2326
2327 const ClonedStateParallelPass = struct {
2328 fn clone(raw: ?*anyopaque, clone_allocator: std.mem.Allocator) anyerror!?*anyopaque {
2329 const state = raw orelse return error.TestMissingState;
2330 const original: *ClonedPassState = @ptrCast(@alignCast(state));
2331 const cloned = try clone_allocator.create(ClonedPassState);
2332 cloned.* = .{
2333 .stats = original.stats,
2334 .original = false,
2335 };
2336 _ = original.stats.clones.fetchAdd(1, .acq_rel);
2337 return cloned;
2338 }
2339
2340 fn deinit(raw: ?*anyopaque, deinit_allocator: std.mem.Allocator) void {
2341 const state: *ClonedPassState = @ptrCast(@alignCast(raw orelse return));
2342 _ = state.stats.deinit_count.fetchAdd(1, .acq_rel);
2343 deinit_allocator.destroy(state);
2344 }
2345
2346 fn run(raw: ?*anyopaque, ctx: *PassContext) PassResult {
2347 if (!ctx.ir_ctx.isMultithreadedExecution()) return .failure;
2348 const state: *ClonedPassState = @ptrCast(@alignCast(raw orelse return .failure));
2349 if (state.original) return .failure;
2350 if (state.runs != 0) return .failure;
2351 state.runs += 1;
2352 _ = state.stats.runs.fetchAdd(1, .acq_rel);
2353
2354 const arrived = state.stats.started.fetchAdd(1, .acq_rel) + 1;
2355 if (arrived == 2) state.stats.release.store(true, .release);
2356
2357 var spins: usize = 0;
2358 while (!state.stats.release.load(.acquire)) {
2359 spins += 1;
2360 if (spins > 1_000_000) return .failure;
2361 sys.thread.yield();
2362 }
2363 ctx.preserveAllAnalyses();
2364 return .success;
2365 }
2366 };
2367
2368 test "PassManager.runWithOptions clones stateful read-only passes for parallel targets" {
2369 const testing = std.testing;
2370 const test_dialect = @import("../../dialects/fixture/root.zig");
2371
2372 var arena = alloc_arena.Arena.init(std.testing.allocator);
2373 defer arena.deinit();
2374 const allocator = arena.allocator();
2375
2376 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2377 defer ctx.deinit(allocator);
2378
2379 const loc = ir.Location.getUnknown();
2380 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
2381 const module_block = module_op.getBodyBlock();
2382
2383 const first_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "first", &.{});
2384 try module_block.addOperation(first_func.op);
2385 const second_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "second", &.{});
2386 try module_block.addOperation(second_func.op);
2387
2388 var stats = ClonedPassStats{};
2389 const original = try allocator.create(ClonedPassState);
2390 original.* = .{
2391 .stats = &stats,
2392 .original = true,
2393 };
2394
2395 var pm = PassManager.init(allocator);
2396 const func_pm = try pm.nest("test.func");
2397 try func_pm.addPass(Pass{
2398 .name = "cloned-state-parallel-check",
2399 .description = "runs with cloned state per target",
2400 .state = original,
2401 .run_with_state_fn = ClonedStateParallelPass.run,
2402 .state_clone_fn = ClonedStateParallelPass.clone,
2403 .state_deinit_fn = ClonedStateParallelPass.deinit,
2404 .mutation_scope = .read_only,
2405 });
2406
2407 const result = pm.runWithOptions(module_op.op, &ctx, .{ .max_threads = 2 });
2408 try testing.expectEqual(PassResult.success, result);
2409 try testing.expectEqual(@as(usize, 2), stats.clones.load(.acquire));
2410 try testing.expectEqual(@as(usize, 2), stats.runs.load(.acquire));
2411 try testing.expectEqual(@as(usize, 2), stats.deinit_count.load(.acquire));
2412
2413 pm.deinit();
2414 try testing.expectEqual(@as(usize, 3), stats.deinit_count.load(.acquire));
2415 }
2416
2417 const ParallelVerifierReadOnlyPass = struct {
2418 fn run(ctx: *PassContext) PassResult {
2419 ctx.preserveAllAnalyses();
2420 return .success;
2421 }
2422 };
2423
2424 const ParallelFixedPointRerunPass = struct {
2425 var runs = std.atomic.Value(usize).init(0);
2426
2427 fn run(ctx: *PassContext) PassResult {
2428 if (!ctx.ir_ctx.isMultithreadedExecution()) return .failure;
2429 _ = runs.fetchAdd(1, .acq_rel);
2430 ctx.preserveAllAnalyses();
2431 return .success;
2432 }
2433 };
2434
2435 test "PassManager.runWithOptions merges parallel fixed-point skips" {
2436 const testing = std.testing;
2437 const test_dialect = @import("../../dialects/fixture/root.zig");
2438
2439 const allocator = testing.allocator;
2440
2441 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2442 defer ctx.deinit(allocator);
2443 try ir.dialects.loadDialectSpec(&ctx, test_dialect.TestDialect.spec);
2444
2445 const loc = ir.Location.getUnknown();
2446 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
2447 const module_block = module_op.getBodyBlock();
2448 const first_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "first", &.{});
2449 try module_block.addOperation(first_func.op);
2450 const second_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "second", &.{});
2451 try module_block.addOperation(second_func.op);
2452
2453 ParallelFixedPointRerunPass.runs.store(0, .release);
2454
2455 const rerun_pass = Pass{
2456 .name = "parallel-fixed-point-rerun",
2457 .description = "",
2458 .run_fn = ParallelFixedPointRerunPass.run,
2459 .mutation_scope = .read_only,
2460 .rerun_policy = .skip_if_unchanged,
2461 };
2462
2463 var pm = PassManager.init(allocator);
2464 defer pm.deinit();
2465 const func_pm = try pm.nest(test_dialect.TestDialect.FuncOp.operation_name);
2466 try func_pm.addPass(rerun_pass);
2467 try func_pm.addPass(rerun_pass);
2468
2469 const result = pm.runWithOptions(module_op.op, &ctx, .{ .max_threads = 2 });
2470 try testing.expectEqual(PassResult.success, result);
2471 try testing.expectEqual(@as(usize, 2), ParallelFixedPointRerunPass.runs.load(.acquire));
2472 try testing.expectEqual(@as(u64, 2), pm.stats.pass_runs);
2473 try testing.expectEqual(@as(u64, 2), pm.stats.passes_skipped);
2474 }
2475
2476 test "PassManager.runWithOptions merges parallel verifier failures in target order" {
2477 const testing = std.testing;
2478 const test_dialect = @import("../../dialects/fixture/root.zig");
2479
2480 var arena = alloc_arena.Arena.init(std.testing.allocator);
2481 defer arena.deinit();
2482 const allocator = arena.allocator();
2483
2484 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2485 defer ctx.deinit(allocator);
2486 try ir.dialects.loadDialectSpec(&ctx, test_dialect.TestDialect.spec);
2487
2488 const loc = ir.Location.getUnknown();
2489 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
2490 const module_block = module_op.getBodyBlock();
2491
2492 const first_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "first", &.{});
2493 try module_block.addOperation(first_func.op);
2494 try appendInvalidTestConstantToBlock(&ctx, first_func.getEntryBlock());
2495
2496 const nested_module = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
2497 try module_block.addOperation(nested_module.op);
2498 try appendInvalidTestConstantToBlock(&ctx, nested_module.getBodyBlock());
2499
2500 var pm = PassManager.init(allocator);
2501 defer pm.deinit();
2502 pm.enableVerifier();
2503
2504 const any_pm = try pm.nestAny();
2505 try any_pm.addPass(Pass{
2506 .name = "read-only-verify-check",
2507 .description = "lets verifier inspect preexisting invalid IR",
2508 .run_fn = ParallelVerifierReadOnlyPass.run,
2509 .mutation_scope = .read_only,
2510 });
2511
2512 const result = pm.runWithOptions(module_op.op, &ctx, .{ .max_threads = 2 });
2513 try testing.expectEqual(PassResult.failure, result);
2514 try testing.expectEqual(@as(u64, 2), pm.stats.pass_runs);
2515 try testing.expectEqual(@as(u64, 2), pm.stats.verifier_failures);
2516
2517 const failure = pm.getLastVerifierFailure() orelse return error.TestExpectedVerifierFailure;
2518 try testing.expectEqualStrings("read-only-verify-check", failure.pass_name);
2519 try testing.expectEqualStrings(test_dialect.TestDialect.FuncOp.operation_name, failure.target_op_name);
2520 try testing.expectEqual(error.MissingRequiredAttribute, failure.err);
2521 }
2522
2523 const parallel_stress_target_count = 12;
2524
2525 const ParallelStressRecorder = struct {
2526 symbols: [parallel_stress_target_count][]const u8 = undefined,
2527 seen: usize = 0,
2528
2529 fn handle(
2530 context: ?*anyopaque,
2531 diagnostic: *const diagnostics.Diagnostic,
2532 ) !diagnostics.HandlerResult {
2533 const self: *@This() = @ptrCast(@alignCast(context.?));
2534 const op = diagnostic.operation orelse return error.TestMissingDiagnosticOperation;
2535 const symbol = ir.SymbolTable.getSymbolName(op) orelse return error.TestMissingSymbol;
2536 self.symbols[self.seen] = symbol;
2537 self.seen += 1;
2538 return .consumed;
2539 }
2540 };
2541
2542 const ParallelStressPassState = struct {
2543 attr_value: i64,
2544 started: std.atomic.Value(usize) = std.atomic.Value(usize).init(0),
2545 release: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
2546 total_runs: std.atomic.Value(usize) = std.atomic.Value(usize).init(0),
2547 };
2548
2549 const ParallelStressPass = struct {
2550 fn run(raw: ?*anyopaque, ctx: *PassContext) PassResult {
2551 const state: *ParallelStressPassState = @ptrCast(@alignCast(raw orelse return .failure));
2552 if (!ctx.ir_ctx.isMultithreadedExecution()) return .failure;
2553 if (ctx.workerCount(parallel_stress_target_count) < 2) return .failure;
2554 _ = ctx.ir_ctx.getI64Attr(state.attr_value) catch return .failure;
2555
2556 const arrived = state.started.fetchAdd(1, .acq_rel) + 1;
2557 if (arrived == 2) state.release.store(true, .release);
2558
2559 var spins: usize = 0;
2560 while (!state.release.load(.acquire)) {
2561 spins += 1;
2562 if (spins > 1_000_000) return .failure;
2563 sys.thread.yield();
2564 }
2565
2566 var diagnostic = ctx.op.emitWarning("parallel stress target");
2567 defer diagnostic.deinit();
2568 _ = diagnostic.emit() catch return .failure;
2569
2570 _ = state.total_runs.fetchAdd(1, .acq_rel);
2571 ctx.preserveAllAnalyses();
2572 return .success;
2573 }
2574 };
2575
2576 const ParallelStressForcedFailurePass = struct {
2577 fn run(ctx: *PassContext) PassResult {
2578 const symbol = ir.SymbolTable.getSymbolName(ctx.op) orelse return .failure;
2579 ctx.preserveAllAnalyses();
2580 if (std.mem.eql(u8, symbol, "fail_7")) return .failure;
2581 return .success;
2582 }
2583 };
2584
2585 const ParallelStressReadOnlyPass = struct {
2586 fn run(ctx: *PassContext) PassResult {
2587 ctx.preserveAllAnalyses();
2588 return .success;
2589 }
2590 };
2591
2592 const ParallelStressStorageMissPass = struct {
2593 fn run(ctx: *PassContext) PassResult {
2594 _ = ctx.ir_ctx.getI64Attr(9999) catch return .failure;
2595 ctx.preserveAllAnalyses();
2596 return .success;
2597 }
2598 };
2599
2600 test "PassManager.runWithOptions stresses read-only parallel target runs" {
2601 const testing = std.testing;
2602 const test_dialect = @import("../../dialects/fixture/root.zig");
2603
2604 for (0..4) |iteration| {
2605 var arena = alloc_arena.Arena.init(std.testing.allocator);
2606 defer arena.deinit();
2607 const allocator = arena.allocator();
2608
2609 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2610 defer ctx.deinit(allocator);
2611 try ir.dialects.loadDialectSpec(&ctx, test_dialect.TestDialect.spec);
2612 _ = try ctx.getI64Attr(@intCast(40 + iteration));
2613
2614 var recorder = ParallelStressRecorder{};
2615 _ = try ctx.registerDiagnosticHandler(.{
2616 .context = &recorder,
2617 .handle = ParallelStressRecorder.handle,
2618 });
2619
2620 const loc = ir.Location.getUnknown();
2621 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
2622 const module_block = module_op.getBodyBlock();
2623 var expected_symbols: [parallel_stress_target_count][]const u8 = undefined;
2624
2625 for (0..parallel_stress_target_count) |slot| {
2626 const ordinal = (slot * 5 + iteration * 3) % parallel_stress_target_count;
2627 const name = try std.fmt.allocPrint(allocator, "stress_{d}_{d}", .{ iteration, ordinal });
2628 expected_symbols[slot] = name;
2629 const func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, name, &.{});
2630 try module_block.addOperation(func.op);
2631 }
2632
2633 var state = ParallelStressPassState{ .attr_value = @intCast(40 + iteration) };
2634 var pm = PassManager.init(allocator);
2635 defer pm.deinit();
2636 pm.enableVerifier();
2637
2638 const func_pm = try pm.nest(test_dialect.TestDialect.FuncOp.operation_name);
2639 try func_pm.addPass(Pass{
2640 .name = "stress-read-only-parallel",
2641 .description = "stress read-only parallel target execution",
2642 .state = &state,
2643 .run_with_state_fn = ParallelStressPass.run,
2644 .state_concurrency = .shared,
2645 .mutation_scope = .read_only,
2646 });
2647
2648 const result = pm.runWithOptions(module_op.op, &ctx, .{ .max_threads = 4 });
2649 try testing.expectEqual(PassResult.success, result);
2650 try testing.expectEqual(@as(u64, parallel_stress_target_count), pm.stats.pass_runs);
2651 try testing.expectEqual(@as(u64, 0), pm.stats.pass_failures);
2652 try testing.expectEqual(@as(u64, 0), pm.stats.verifier_failures);
2653 try testing.expectEqual(@as(u64, 0), pm.stats.passes_modified);
2654 try testing.expectEqual(
2655 @as(usize, parallel_stress_target_count),
2656 state.total_runs.load(.acquire),
2657 );
2658 try testing.expectEqual(@as(usize, parallel_stress_target_count), recorder.seen);
2659 for (expected_symbols, 0..) |expected, index| {
2660 try testing.expectEqualStrings(expected, recorder.symbols[index]);
2661 }
2662 }
2663
2664 {
2665 var arena = alloc_arena.Arena.init(std.testing.allocator);
2666 defer arena.deinit();
2667 const allocator = arena.allocator();
2668
2669 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2670 defer ctx.deinit(allocator);
2671 try ir.dialects.loadDialectSpec(&ctx, test_dialect.TestDialect.spec);
2672
2673 const loc = ir.Location.getUnknown();
2674 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
2675 const module_block = module_op.getBodyBlock();
2676 for (0..parallel_stress_target_count) |index| {
2677 const name = try std.fmt.allocPrint(allocator, "fail_{d}", .{index});
2678 const func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, name, &.{});
2679 try module_block.addOperation(func.op);
2680 }
2681
2682 var pm = PassManager.init(allocator);
2683 defer pm.deinit();
2684 const func_pm = try pm.nest(test_dialect.TestDialect.FuncOp.operation_name);
2685 try func_pm.addPass(Pass{
2686 .name = "stress-forced-failure",
2687 .description = "fails one parallel target",
2688 .run_fn = ParallelStressForcedFailurePass.run,
2689 .mutation_scope = .read_only,
2690 });
2691
2692 const result = pm.runWithOptions(module_op.op, &ctx, .{ .max_threads = 4 });
2693 try testing.expectEqual(PassResult.failure, result);
2694 try testing.expectEqual(@as(u64, parallel_stress_target_count), pm.stats.pass_runs);
2695 try testing.expectEqual(@as(u64, 1), pm.stats.pass_failures);
2696
2697 const reproducer = pm.getLastFailureReproducer() orelse return error.TestExpectedReproducer;
2698 try testing.expectEqual(PassFailureKind.pass, reproducer.failure_kind.?);
2699 try testing.expectEqualStrings("fail_7", reproducer.target_symbol_name.?);
2700 }
2701
2702 {
2703 var arena = alloc_arena.Arena.init(std.testing.allocator);
2704 defer arena.deinit();
2705 const allocator = arena.allocator();
2706
2707 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2708 defer ctx.deinit(allocator);
2709 try ir.dialects.loadDialectSpec(&ctx, test_dialect.TestDialect.spec);
2710
2711 const loc = ir.Location.getUnknown();
2712 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
2713 const module_block = module_op.getBodyBlock();
2714 var invalid_targets: u64 = 0;
2715 for (0..parallel_stress_target_count) |index| {
2716 const name = try std.fmt.allocPrint(allocator, "verify_{d}", .{index});
2717 const func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, name, &.{});
2718 try module_block.addOperation(func.op);
2719 if (index % 3 == 1) {
2720 try appendInvalidTestConstantToBlock(&ctx, func.getEntryBlock());
2721 invalid_targets += 1;
2722 }
2723 }
2724
2725 var pm = PassManager.init(allocator);
2726 defer pm.deinit();
2727 pm.enableVerifier();
2728 const func_pm = try pm.nest(test_dialect.TestDialect.FuncOp.operation_name);
2729 try func_pm.addPass(Pass{
2730 .name = "stress-verifier-failure",
2731 .description = "lets verifier fail invalid parallel targets",
2732 .run_fn = ParallelStressReadOnlyPass.run,
2733 .mutation_scope = .read_only,
2734 });
2735
2736 const result = pm.runWithOptions(module_op.op, &ctx, .{ .max_threads = 4 });
2737 try testing.expectEqual(PassResult.failure, result);
2738 try testing.expectEqual(@as(u64, parallel_stress_target_count), pm.stats.pass_runs);
2739 try testing.expectEqual(invalid_targets, pm.stats.verifier_failures);
2740
2741 const reproducer = pm.getLastFailureReproducer() orelse return error.TestExpectedReproducer;
2742 try testing.expectEqual(PassFailureKind.verifier, reproducer.failure_kind.?);
2743 try testing.expectEqual(error.MissingRequiredAttribute, reproducer.verifier_error.?);
2744 try testing.expectEqualStrings("MissingRequiredAttribute", reproducer.verifier_error_name.?);
2745 }
2746
2747 {
2748 var arena = alloc_arena.Arena.init(std.testing.allocator);
2749 defer arena.deinit();
2750 const allocator = arena.allocator();
2751
2752 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2753 defer ctx.deinit(allocator);
2754 try ir.dialects.loadDialectSpec(&ctx, test_dialect.TestDialect.spec);
2755
2756 const loc = ir.Location.getUnknown();
2757 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
2758 const module_block = module_op.getBodyBlock();
2759 for (0..4) |index| {
2760 const name = try std.fmt.allocPrint(allocator, "miss_{d}", .{index});
2761 const func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, name, &.{});
2762 try module_block.addOperation(func.op);
2763 }
2764
2765 var pm = PassManager.init(allocator);
2766 defer pm.deinit();
2767 const func_pm = try pm.nest(test_dialect.TestDialect.FuncOp.operation_name);
2768 try func_pm.addPass(Pass{
2769 .name = "stress-context-storage-miss",
2770 .description = "fails on context storage miss",
2771 .run_fn = ParallelStressStorageMissPass.run,
2772 .mutation_scope = .read_only,
2773 });
2774
2775 const result = pm.runWithOptions(module_op.op, &ctx, .{ .max_threads = 4 });
2776 try testing.expectEqual(PassResult.failure, result);
2777 try testing.expectEqual(@as(u64, 4), pm.stats.pass_runs);
2778 try testing.expectEqual(@as(u64, 4), pm.stats.pass_failures);
2779 }
2780 }
2781
2782 const SerialThreadingCheckPass = struct {
2783 fn run(ctx: *PassContext) PassResult {
2784 if (ctx.ir_ctx.isMultithreadedExecution()) return .failure;
2785 ctx.preserveAllAnalyses();
2786 return .success;
2787 }
2788 };
2789
2790 test "PassManager.run keeps serial passes outside multithreaded execution" {
2791 const testing = std.testing;
2792 const test_dialect = @import("../../dialects/fixture/root.zig");
2793
2794 var arena = alloc_arena.Arena.init(std.testing.allocator);
2795 defer arena.deinit();
2796 const allocator = arena.allocator();
2797
2798 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2799 defer ctx.deinit(allocator);
2800
2801 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
2802
2803 var pm = PassManager.init(allocator);
2804 defer pm.deinit();
2805 try pm.addPass(Pass{
2806 .name = "serial-threading-check",
2807 .description = "checks serial context execution state",
2808 .run_fn = SerialThreadingCheckPass.run,
2809 .mutation_scope = .read_only,
2810 });
2811
2812 const result = pm.run(module_op.op, &ctx);
2813 try testing.expectEqual(PassResult.success, result);
2814 }
2815
2816 const WorkerAllocatorCheckPass = struct {
2817 var expected: std.mem.Allocator = undefined;
2818 var seen = std.atomic.Value(usize).init(0);
2819
2820 fn run(ctx: *PassContext) PassResult {
2821 if (ctx.allocator.ptr != expected.ptr) return .failure;
2822 if (ctx.allocator.vtable != expected.vtable) return .failure;
2823 const marker = ctx.allocator.create(u32) catch return .failure;
2824 ctx.allocator.destroy(marker);
2825 ctx.preserveAllAnalyses();
2826 _ = seen.fetchAdd(1, .acq_rel);
2827 return .success;
2828 }
2829 };
2830
2831 test "PassManager.runWithOptions uses configured worker allocator for read-only targets" {
2832 const testing = std.testing;
2833 const test_dialect = @import("../../dialects/fixture/root.zig");
2834
2835 var arena = alloc_arena.Arena.init(std.testing.allocator);
2836 defer arena.deinit();
2837 const allocator = arena.allocator();
2838
2839 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2840 defer ctx.deinit(allocator);
2841
2842 const loc = ir.Location.getUnknown();
2843 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
2844 const module_block = module_op.getBodyBlock();
2845
2846 const first_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "first", &.{});
2847 try module_block.addOperation(first_func.op);
2848 const second_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "second", &.{});
2849 try module_block.addOperation(second_func.op);
2850
2851 WorkerAllocatorCheckPass.seen.store(0, .release);
2852
2853 var pm = PassManager.init(allocator);
2854 defer pm.deinit();
2855
2856 const func_pm = try pm.nest("test.func");
2857 try func_pm.addPass(Pass{
2858 .name = "worker-allocator-check",
2859 .description = "checks read-only worker allocator",
2860 .run_fn = WorkerAllocatorCheckPass.run,
2861 .mutation_scope = .read_only,
2862 });
2863
2864 var worker_gpa = alloc_observe.debug.Allocator(.{}).init(
2865 testing.allocator,
2866 );
2867 defer {
2868 const status = worker_gpa.deinit();
2869 testing.expect(status == .ok) catch @panic("pass worker allocator leaked allocations");
2870 }
2871 const worker_allocator = worker_gpa.allocator();
2872 WorkerAllocatorCheckPass.expected = worker_allocator;
2873
2874 const result = pm.runWithOptions(module_op.op, &ctx, .{
2875 .max_threads = 2,
2876 .worker_allocator = worker_allocator,
2877 });
2878 try testing.expectEqual(PassResult.success, result);
2879 try testing.expectEqual(@as(usize, 2), WorkerAllocatorCheckPass.seen.load(.acquire));
2880 }
2881
2882 const RunOptionsCheckPass = struct {
2883 var expected_allocator: std.mem.Allocator = undefined;
2884
2885 fn run(ctx: *PassContext) PassResult {
2886 if (ctx.workerCount(8) != 3) return .failure;
2887 const worker_allocator = ctx.workerAllocator();
2888 if (worker_allocator.ptr != expected_allocator.ptr) return .failure;
2889 if (worker_allocator.vtable != expected_allocator.vtable) return .failure;
2890 ctx.preserveAllAnalyses();
2891 return .success;
2892 }
2893 };
2894
2895 test "PassManager.runWithOptions exposes options to pass context" {
2896 const testing = std.testing;
2897 const test_dialect = @import("../../dialects/fixture/root.zig");
2898
2899 var arena = alloc_arena.Arena.init(std.testing.allocator);
2900 defer arena.deinit();
2901 const allocator = arena.allocator();
2902
2903 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2904 defer ctx.deinit(allocator);
2905
2906 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
2907
2908 var pm = PassManager.init(allocator);
2909 defer pm.deinit();
2910 try pm.addPass(Pass{
2911 .name = "option-check",
2912 .description = "checks pass context run options",
2913 .run_fn = RunOptionsCheckPass.run,
2914 .mutation_scope = .read_only,
2915 });
2916
2917 var worker_gpa = alloc_observe.debug.Allocator(.{}).init(
2918 testing.allocator,
2919 );
2920 defer {
2921 const status = worker_gpa.deinit();
2922 testing.expect(status == .ok) catch @panic("pass option worker allocator leaked allocations");
2923 }
2924 const worker_allocator = worker_gpa.allocator();
2925 RunOptionsCheckPass.expected_allocator = worker_allocator;
2926
2927 const result = pm.runWithOptions(module_op.op, &ctx, .{
2928 .max_threads = 3,
2929 .worker_allocator = worker_allocator,
2930 });
2931 try testing.expectEqual(PassResult.success, result);
2932 }
2933
2934 const SameTypeFuncCounter = struct {
2935 var count: usize = 0;
2936
2937 fn run(ctx: *PassContext) PassResult {
2938 if (std.mem.eql(u8, ctx.op.name.name, "test.func")) {
2939 count += 1;
2940 }
2941 return .success;
2942 }
2943 };
2944
2945 test "Same-type nesting: func inside func both processed" {
2946 const testing = std.testing;
2947 const test_dialect = @import("../../dialects/fixture/root.zig");
2948
2949 var arena = alloc_arena.Arena.init(std.testing.allocator);
2950 defer arena.deinit();
2951 const allocator = arena.allocator();
2952
2953 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2954 defer ctx.deinit(allocator);
2955
2956 const loc = ir.Location.getUnknown();
2957 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
2958 const module_block = module_op.getBodyBlock();
2959
2960 const outer_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "outer", &.{});
2961 try module_block.addOperation(outer_func.op);
2962
2963 const inner_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "inner", &.{});
2964 try outer_func.getEntryBlock().addOperation(inner_func.op);
2965
2966 var pm = PassManager.init(allocator);
2967 defer pm.deinit();
2968
2969 SameTypeFuncCounter.count = 0;
2970
2971 const func_pm = try pm.nest("test.func");
2972 try func_pm.addPass(Pass{
2973 .name = "func-counter",
2974 .description = "Counts func ops",
2975 .run_fn = SameTypeFuncCounter.run,
2976 });
2977
2978 const result = pm.run(module_op.op, &ctx);
2979 try testing.expectEqual(PassResult.success, result);
2980
2981 try testing.expectEqual(@as(usize, 2), SameTypeFuncCounter.count);
2982 }
2983
2984 test "PassManager with instrumentation invokes hooks" {
2985 const testing = std.testing;
2986 const test_dialect = @import("../../dialects/fixture/root.zig");
2987
2988 var arena = alloc_arena.Arena.init(std.testing.allocator);
2989 defer arena.deinit();
2990 const allocator = arena.allocator();
2991
2992 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2993 defer ctx.deinit(allocator);
2994
2995 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
2996
2997 var pm = PassManager.init(allocator);
2998 defer pm.deinit();
2999
3000 var counter = CountingInstrumentation{};
3001 try pm.addInstrumentation(counter.instrumentation());
3002
3003 try pm.addPass(Pass{
3004 .name = "noop-pass",
3005 .description = "Does nothing",
3006 .run_fn = test_pass_success,
3007 });
3008
3009 const result = pm.run(module_op.op, &ctx);
3010 try testing.expectEqual(PassResult.success, result);
3011
3012 try testing.expectEqual(@as(usize, 1), counter.pipeline_count);
3013 try testing.expectEqual(@as(usize, 1), counter.pass_count);
3014 try testing.expectEqual(@as(usize, 0), counter.pass_failures);
3015 }
3016
3017 test "PassManager timing instrumentation" {
3018 const testing = std.testing;
3019 const test_dialect = @import("../../dialects/fixture/root.zig");
3020
3021 var arena = alloc_arena.Arena.init(std.testing.allocator);
3022 defer arena.deinit();
3023 const allocator = arena.allocator();
3024
3025 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
3026 defer ctx.deinit(allocator);
3027
3028 const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
3029
3030 var pm = PassManager.init(allocator);
3031 defer pm.deinit();
3032
3033 var timing = TimingInstrumentation.init(allocator);
3034 defer timing.deinit();
3035 try pm.addInstrumentation(timing.instrumentation());
3036
3037 try pm.addPass(Pass{
3038 .name = "timed-pass",
3039 .description = "Timed pass",
3040 .run_fn = test_pass_success,
3041 });
3042
3043 const result = pm.run(module_op.op, &ctx);
3044 try testing.expectEqual(PassResult.success, result);
3045
3046 try testing.expect(timing.getPassCount("timed-pass") != null);
3047 try testing.expectEqual(@as(u64, 1), timing.getPassCount("timed-pass").?);
3048 try testing.expect(timing.hasPipelineTimings());
3049
3050 const pipeline_summaries = try timing.pipelineSummariesAlloc(allocator);
3051 try testing.expectEqual(@as(usize, 1), pipeline_summaries.len);
3052 try testing.expectEqual(@as(?[]const u8, null), pipeline_summaries[0].target);
3053 try testing.expectEqual(@as(usize, 0), pipeline_summaries[0].depth);
3054 try testing.expect(pipeline_summaries[0].total_ns >= 0);
3055 }
3056
3057 fn meteredAnalysisBounds(_: subject.work.Input) !subject.work.Bounds {
3058 return .{
3059 .work = .{
3060 .analysis_computations = 1,
3061 .structural_visits = 5,
3062 .allocation_capacity = 4096,
3063 },
3064 .workspace = 4096,
3065 };
3066 }
3067
3068 test "U0 analysis admission refuses before computation and keeps the exhausted charge" {
3069 const revision = @import("../../product/revision/root.zig");
3070 const fixture = @import("../../dialects/fixture/root.zig");
3071 const allocator = std.testing.allocator;
3072 var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
3073 defer context.deinit(allocator);
3074 const module = try fixture.TestDialect.ModuleOp.create(&context, ir.Location.getUnknown());
3075 const ledger = try revision.AccountingV1.create(allocator, .{
3076 .allowance = .{
3077 .structural_visits = 4,
3078 .analysis_computations = 1,
3079 .allocation_capacity = 65536,
3080 },
3081 .workspace = 4096,
3082 .events = 8,
3083 }, &.{});
3084 defer ledger.destroy();
3085 var stats: PassManagerStats = .{};
3086 var cache = try AnalysisCache.initAccounted(allocator, &stats, ledger, .{}, 4);
3087 defer cache.deinit();
3088 var notifications = CountingInstrumentation{};
3089 var instrumentor = PassInstrumentor.init(allocator);
3090 defer instrumentor.deinit();
3091 try instrumentor.addInstrumentation(notifications.instrumentation());
3092 var ctx = PassContext.initWithInstrumentor(
3093 module.op,
3094 &context,
3095 allocator,
3096 &cache,
3097 &instrumentor,
3098 );
3099 defer ctx.deinit();
3100 const descriptor = AnalysisDescriptor{
3101 .id = analysisId("u0-metered-analysis"),
3102 .name = "u0-metered-analysis",
3103 .work_contract = .{
3104 .identity = .{ .name = "u0-metered-analysis", .version = 1 },
3105 .estimate = meteredAnalysisBounds,
3106 },
3107 };
3108 analysis_counter = 0;
3109 const result = ctx.getAnalysis(module.op, &descriptor, computeCounter, cleanupCounter);
3110 try std.testing.expectError(error.WorkExhausted, result);
3111 try std.testing.expectEqual(0, analysis_counter);
3112 try std.testing.expectEqual(0, stats.analysis_misses);
3113 try std.testing.expectEqual(0, notifications.analysis_count);
3114 try std.testing.expectEqual(.exhausted, ledger.view().outcome);
3115 try std.testing.expectEqual(5, ledger.view().charged.structural_visits);
3116 try std.testing.expectEqual(0, cache.entries.count());
3117 }
3118
3119 test "U0 analysis accounting records computations and hits without charging a second computation" {
3120 const revision = @import("../../product/revision/root.zig");
3121 const fixture = @import("../../dialects/fixture/root.zig");
3122 const allocator = std.testing.allocator;
3123 var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
3124 defer context.deinit(allocator);
3125 const module = try fixture.TestDialect.ModuleOp.create(&context, ir.Location.getUnknown());
3126 const pipeline = [_]revision.record.Version{.{ .name = "host", .version = 1 }};
3127 const ledger = try revision.AccountingV1.create(allocator, .{
3128 .allowance = .{
3129 .structural_visits = 6,
3130 .analysis_computations = 1,
3131 .allocation_capacity = 65536,
3132 },
3133 .workspace = 4096,
3134 .events = 8,
3135 }, &pipeline);
3136 defer ledger.destroy();
3137 var stats: PassManagerStats = .{};
3138 var cache = try AnalysisCache.initAccounted(allocator, &stats, ledger, .{}, 4);
3139 defer cache.deinit();
3140 const parent = try ledger.begin(.pass, .{
3141 .identity = pipeline[0],
3142 .work = .{ .structural_visits = 1 },
3143 });
3144 var ctx = PassContext.init(module.op, &context, allocator, &cache);
3145 defer ctx.deinit();
3146 const descriptor = AnalysisDescriptor{
3147 .id = analysisId("u0-metered-analysis"),
3148 .name = "u0-metered-analysis",
3149 .work_contract = .{
3150 .identity = .{ .name = "u0-metered-analysis", .version = 1 },
3151 .estimate = meteredAnalysisBounds,
3152 },
3153 };
3154 analysis_counter = 0;
3155 const first = try ctx.getAnalysis(module.op, &descriptor, computeCounter, cleanupCounter);
3156 const second = try ctx.getAnalysis(module.op, &descriptor, computeCounter, cleanupCounter);
3157 try ledger.finish(parent, .success, .{ .counters = .{ .pass_runs = 1 } });
3158 try std.testing.expectEqual(first, second);
3159 try std.testing.expectEqual(1, analysis_counter);
3160 try std.testing.expectEqual(1, ledger.view().charged.analysis_computations);
3161 try std.testing.expectEqual(
3162 stats.analysis_misses,
3163 ledger.view().executed.counters.analysis_misses,
3164 );
3165 try std.testing.expectEqual(stats.analysis_hits, ledger.view().executed.counters.analysis_hits);
3166 try std.testing.expectEqual(1, ledger.view().events[parent].executed.counters.analysis_hits);
3167 try std.testing.expectEqual(.analysis, ledger.view().events[2].phase);
3168 try std.testing.expectEqual(parent, ledger.view().events[2].parent.?);
3169 try std.testing.expectEqual(1, ledger.view().executed.counters.pass_runs);
3170 try ledger.producersComplete();
3171 }
3172
3173 test "U0 analysis missing contract permits transient computation and refuses qualification" {
3174 const revision = @import("../../product/revision/root.zig");
3175 const fixture = @import("../../dialects/fixture/root.zig");
3176 const allocator = std.testing.allocator;
3177 var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
3178 defer context.deinit(allocator);
3179 const module = try fixture.TestDialect.ModuleOp.create(&context, ir.Location.getUnknown());
3180 const ledger = try revision.AccountingV1.create(allocator, .{
3181 .allowance = .{ .allocation_capacity = 65536 },
3182 .workspace = 4096,
3183 .events = 8,
3184 }, &.{});
3185 defer ledger.destroy();
3186 var cache = try AnalysisCache.initAccounted(allocator, null, ledger, .{}, 4);
3187 defer cache.deinit();
3188 var ctx = PassContext.init(module.op, &context, allocator, &cache);
3189 defer ctx.deinit();
3190 const descriptor = AnalysisDescriptor{
3191 .id = analysisId("u0-unmetered-analysis"),
3192 .name = "u0-unmetered-analysis",
3193 };
3194 analysis_counter = 0;
3195 _ = try ctx.getAnalysis(module.op, &descriptor, computeCounter, cleanupCounter);
3196 try std.testing.expectEqual(1, analysis_counter);
3197 try std.testing.expectEqual(1, ledger.view().executed.counters.analysis_misses);
3198 try std.testing.expectError(error.MissingWorkContract, ledger.producersComplete());
3199 try std.testing.expect(ledger.view().missing_work_contract);
3200 try std.testing.expectEqual(1, ledger.view().events.len);
3201 }
3202
3203 fn computeMeteredTyped(ctx: *PassContext, op: *ir.Operation) anyerror!*anyopaque {
3204 return @ptrCast(try computeTypedCounter(ctx, op));
3205 }
3206
3207 fn cleanupMeteredTyped(value: *anyopaque, allocator: std.mem.Allocator) void {
3208 cleanupTypedCounter(@ptrCast(@alignCast(value)), allocator);
3209 }
3210
3211 test "U0 analysis computation failure stays charged and cannot retry the same request" {
3212 const revision = @import("../../product/revision/root.zig");
3213 const fixture = @import("../../dialects/fixture/root.zig");
3214 const allocator = std.testing.allocator;
3215 var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
3216 defer context.deinit(allocator);
3217 const module = try fixture.TestDialect.ModuleOp.create(&context, ir.Location.getUnknown());
3218 const ledger = try revision.AccountingV1.create(allocator, .{
3219 .allowance = .{
3220 .structural_visits = 5,
3221 .analysis_computations = 1,
3222 .allocation_capacity = 65536,
3223 },
3224 .workspace = 4096,
3225 .events = 8,
3226 }, &.{});
3227 defer ledger.destroy();
3228 var failing = std.testing.FailingAllocator.init(allocator, .{ .fail_index = 1 });
3229 var stats: PassManagerStats = .{};
3230 var cache = try AnalysisCache.initAccounted(failing.allocator(), &stats, ledger, .{}, 4);
3231 defer cache.deinit();
3232 var ctx = PassContext.init(module.op, &context, failing.allocator(), &cache);
3233 defer ctx.deinit();
3234 const descriptor = AnalysisDescriptor{
3235 .id = analysisId("u0-metered-analysis"),
3236 .name = "u0-metered-analysis",
3237 .work_contract = .{
3238 .identity = .{ .name = "u0-metered-analysis", .version = 1 },
3239 .estimate = meteredAnalysisBounds,
3240 },
3241 };
3242 typed_analysis_counter = 0;
3243 typed_analysis_cleanup_count = 0;
3244 try std.testing.expectError(
3245 error.OutOfMemory,
3246 ctx.getAnalysis(module.op, &descriptor, computeMeteredTyped, cleanupMeteredTyped),
3247 );
3248 try std.testing.expect(failing.has_induced_failure);
3249 try std.testing.expectEqual(1, typed_analysis_counter);
3250 try std.testing.expectEqual(0, typed_analysis_cleanup_count);
3251 try std.testing.expectEqual(0, cache.entries.count());
3252 try std.testing.expectEqual(.rejected, ledger.view().outcome);
3253 try std.testing.expectEqual(5, ledger.view().charged.structural_visits);
3254 try std.testing.expectEqual(1, ledger.view().executed.work.analysis_computations);
3255 try std.testing.expectEqual(0, ledger.view().executed.counters.analysis_misses);
3256 failing.fail_index = std.math.maxInt(usize);
3257 try std.testing.expectError(
3258 error.TerminalWorkOutcome,
3259 ctx.getAnalysis(module.op, &descriptor, computeMeteredTyped, cleanupMeteredTyped),
3260 );
3261 try std.testing.expectEqual(1, typed_analysis_counter);
3262 }
3263
3264 fn refreshMeteredCounter(_: *PassContext, _: *ir.Operation, value: *anyopaque) !void {
3265 analysis_counter += 1;
3266 const counter: *u32 = @ptrCast(@alignCast(value));
3267 counter.* = analysis_counter;
3268 }
3269
3270 test "U0 analysis refresh charges before replacing a cached value" {
3271 const revision = @import("../../product/revision/root.zig");
3272 const fixture = @import("../../dialects/fixture/root.zig");
3273 const allocator = std.testing.allocator;
3274 var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
3275 defer context.deinit(allocator);
3276 const module = try fixture.TestDialect.ModuleOp.create(&context, ir.Location.getUnknown());
3277 const descriptor = AnalysisDescriptor{
3278 .id = analysisId("u0-metered-analysis"),
3279 .name = "u0-metered-analysis",
3280 .work_contract = .{
3281 .identity = .{ .name = "u0-metered-analysis", .version = 1 },
3282 .estimate = meteredAnalysisBounds,
3283 },
3284 };
3285 for ([_]u64{ 1, 2 }) |computations| {
3286 const ledger = try revision.AccountingV1.create(allocator, .{
3287 .allowance = .{
3288 .structural_visits = 10,
3289 .analysis_computations = computations,
3290 .allocation_capacity = 65536,
3291 },
3292 .workspace = 4096,
3293 .events = 8,
3294 }, &.{});
3295 defer ledger.destroy();
3296 var stats: PassManagerStats = .{};
3297 var cache = try AnalysisCache.initAccounted(allocator, &stats, ledger, .{}, 4);
3298 defer cache.deinit();
3299 var ctx = PassContext.init(module.op, &context, allocator, &cache);
3300 defer ctx.deinit();
3301 analysis_counter = 0;
3302 const first = try ctx.getAnalysis(module.op, &descriptor, computeCounter, cleanupCounter);
3303 const result = ctx.refreshAnalysis(module.op, &descriptor, refreshMeteredCounter);
3304 if (computations == 1) {
3305 try std.testing.expectError(error.WorkExhausted, result);
3306 try std.testing.expectEqual(1, @as(*u32, @ptrCast(@alignCast(first))).*);
3307 try std.testing.expectEqual(.exhausted, ledger.view().outcome);
3308 } else {
3309 try result;
3310 const next = try ctx.getAnalysis(
3311 module.op,
3312 &descriptor,
3313 computeCounter,
3314 cleanupCounter,
3315 );
3316 try std.testing.expectEqual(first, next);
3317 try std.testing.expectEqual(2, @as(*u32, @ptrCast(@alignCast(next))).*);
3318 }
3319 try std.testing.expectEqual(computations, analysis_counter);
3320 try std.testing.expectEqual(computations, stats.analysis_misses);
3321 try std.testing.expectEqual(2, ledger.view().charged.analysis_computations);
3322 }
3323 }
3324
3325 const MeteredTypedAnalysis = Analysis(
3326 u32,
3327 "u0-metered-typed-analysis",
3328 &.{},
3329 computeTypedCounter,
3330 cleanupTypedCounter,
3331 .{
3332 .identity = .{ .name = "u0-metered-typed-analysis", .version = 1 },
3333 .estimate = meteredAnalysisBounds,
3334 },
3335 );
3336
3337 test "U0 analysis typed registration carries its normative declaration" {
3338 const revision = @import("../../product/revision/root.zig");
3339 const fixture = @import("../../dialects/fixture/root.zig");
3340 const allocator = std.testing.allocator;
3341 var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
3342 defer context.deinit(allocator);
3343 const module = try fixture.TestDialect.ModuleOp.create(&context, ir.Location.getUnknown());
3344 const ledger = try revision.AccountingV1.create(allocator, .{
3345 .allowance = .{ .analysis_computations = 0, .allocation_capacity = 65536 },
3346 .workspace = 4096,
3347 .events = 8,
3348 }, &.{});
3349 defer ledger.destroy();
3350 var cache = try AnalysisCache.initAccounted(allocator, null, ledger, .{}, 4);
3351 defer cache.deinit();
3352 var ctx = PassContext.init(module.op, &context, allocator, &cache);
3353 defer ctx.deinit();
3354 typed_analysis_counter = 0;
3355 try std.testing.expectError(error.WorkExhausted, MeteredTypedAnalysis.get(&ctx, module.op));
3356 try std.testing.expectEqual(0, typed_analysis_counter);
3357 try std.testing.expect(!ledger.view().missing_work_contract);
3358 try std.testing.expectEqualStrings(
3359 "u0-metered-typed-analysis",
3360 ledger.view().events[1].identity().name,
3361 );
3362 }
3363
3364 test "U0 analysis fixed workspace exhaustion retains cleanup and refuses a fresh retry" {
3365 const revision = @import("../../product/revision/root.zig");
3366 const fixture = @import("../../dialects/fixture/root.zig");
3367 const fixed = @import("alloc_fixed");
3368 const allocator = std.testing.allocator;
3369 var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
3370 defer context.deinit(allocator);
3371 const module = try fixture.TestDialect.ModuleOp.create(&context, ir.Location.getUnknown());
3372 const ledger = try revision.AccountingV1.create(allocator, .{
3373 .allowance = .{
3374 .structural_visits = 5,
3375 .analysis_computations = 1,
3376 .allocation_capacity = 65536,
3377 },
3378 .workspace = 4096,
3379 .events = 8,
3380 }, &.{});
3381 defer ledger.destroy();
3382 var bytes: [4096]u8 align(@alignOf(usize)) = undefined;
3383 var storage = fixed.Tracked.init(&bytes);
3384 var cache = try AnalysisCache.initAccounted(
3385 storage.allocator(),
3386 null,
3387 ledger,
3388 .{ .workspace = &storage.exhausted },
3389 4,
3390 );
3391 defer cache.deinit();
3392 var ctx = PassContext.init(module.op, &context, storage.allocator(), &cache);
3393 defer ctx.deinit();
3394 typed_analysis_counter = 0;
3395 typed_analysis_cleanup_count = 0;
3396 try std.testing.expectError(error.WorkExhausted, OversizedTypedAnalysis.get(&ctx, module.op));
3397 try std.testing.expectEqual(1, typed_analysis_counter);
3398 try std.testing.expectEqual(0, typed_analysis_cleanup_count);
3399 try std.testing.expectEqual(0, cache.entries.count());
3400 try std.testing.expectEqual(.exhausted, ledger.view().outcome);
3401 try std.testing.expectEqual(1, ledger.view().charged.analysis_computations);
3402 try std.testing.expectEqual(0, ledger.view().executed.counters.analysis_misses);
3403 try std.testing.expectError(
3404 error.TerminalWorkOutcome,
3405 OversizedTypedAnalysis.get(&ctx, module.op),
3406 );
3407 try std.testing.expectEqual(1, typed_analysis_counter);
3408 }
3409
3410 test "U0 analysis cache reserves its declared table before computations" {
3411 const revision = @import("../../product/revision/root.zig");
3412 const allocator = std.testing.allocator;
3413 const ledger = try revision.AccountingV1.create(allocator, .{
3414 .allowance = .{ .allocation_capacity = 65536 },
3415 .workspace = 4096,
3416 .events = 8,
3417 }, &.{});
3418 defer ledger.destroy();
3419 var failing = std.testing.FailingAllocator.init(allocator, .{ .fail_index = 0 });
3420 const result = AnalysisCache.initAccounted(failing.allocator(), null, ledger, .{}, 4);
3421 if (result) |value| {
3422 var cache = value;
3423 cache.deinit();
3424 } else |_| {}
3425 try std.testing.expectError(error.OutOfMemory, result);
3426 try std.testing.expect(failing.has_induced_failure);
3427 try std.testing.expectEqual(.rejected, ledger.view().outcome);
3428 try std.testing.expect(ledger.view().charged.allocation_capacity > 0);
3429 try std.testing.expectEqual(0, ledger.view().executed.work.analysis_computations);
3430 }
3431
3432 const OversizedTypedAnalysis = Analysis(
3433 u32,
3434 "u0-oversized-analysis",
3435 &.{},
3436 computeOversizedCounter,
3437 cleanupTypedCounter,
3438 .{
3439 .identity = .{ .name = "u0-oversized-analysis", .version = 1 },
3440 .estimate = oversizedAnalysisBounds,
3441 },
3442 );
3443
3444 fn computeOversizedCounter(ctx: *PassContext, _: *ir.Operation) !*u32 {
3445 typed_analysis_counter += 1;
3446 {
3447 const scratch = try ctx.allocator.alloc(u8, 4096);
3448 defer ctx.allocator.free(scratch);
3449 @memset(scratch, 0);
3450 }
3451 const value = try ctx.allocator.create(u32);
3452 value.* = typed_analysis_counter;
3453 return value;
3454 }
3455
3456 fn oversizedAnalysisBounds(input: subject.work.Input) !subject.work.Bounds {
3457 var bounds = try meteredAnalysisBounds(input);
3458 bounds.work.allocation_capacity = 8192;
3459 return bounds;
3460 }
3461
3462 test "U0 analysis cache storage bound covers real table allocation and teardown" {
3463 const revision = @import("../../product/revision/root.zig");
3464 const allocator = std.testing.allocator;
3465 for ([_]u32{ 0, 1, 6, 7, 8, 13, 64, 1024, 65536 }) |limit| {
3466 const bound = try AnalysisCache.storageBound(limit);
3467 const ledger = try revision.AccountingV1.create(allocator, .{
3468 .allowance = .{ .allocation_capacity = bound },
3469 .workspace = bound,
3470 .events = 8,
3471 }, &.{});
3472 defer ledger.destroy();
3473 var observed = std.testing.FailingAllocator.init(allocator, .{});
3474 var cache = try AnalysisCache.initAccounted(
3475 observed.allocator(),
3476 null,
3477 ledger,
3478 .{},
3479 limit,
3480 );
3481 try std.testing.expect(observed.allocated_bytes <= bound);
3482 try std.testing.expectEqual(@as(usize, if (limit == 0) 0 else 1), observed.allocations);
3483 try std.testing.expectEqual(0, cache.entries.count());
3484 try std.testing.expectEqual(bound, ledger.view().charged.allocation_capacity);
3485 try std.testing.expectEqual(.success, ledger.view().events[0].outcome);
3486 cache.deinit();
3487 try std.testing.expectEqual(observed.allocated_bytes, observed.freed_bytes);
3488 }
3489 try std.testing.expectError(
3490 error.WorkOverflow,
3491 AnalysisCache.storageBound(std.math.maxInt(u32)),
3492 );
3493 }
3494
3495 test "U0 analysis cache refuses its entry limit before the next computation without table growth" {
3496 const revision = @import("../../product/revision/root.zig");
3497 const fixture = @import("../../dialects/fixture/root.zig");
3498 const allocator = std.testing.allocator;
3499 var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
3500 defer context.deinit(allocator);
3501 const module = try fixture.TestDialect.ModuleOp.create(&context, .unknown);
3502 const ledger = try revision.AccountingV1.create(allocator, .{
3503 .allowance = .{
3504 .structural_visits = 10,
3505 .analysis_computations = 2,
3506 .allocation_capacity = 65536,
3507 },
3508 .workspace = 4096,
3509 .events = 8,
3510 }, &.{});
3511 defer ledger.destroy();
3512 var observed = std.testing.FailingAllocator.init(allocator, .{});
3513 var cache = try AnalysisCache.initAccounted(observed.allocator(), null, ledger, .{}, 1);
3514 defer cache.deinit();
3515 const capacity = cache.entries.capacity();
3516 var ctx = PassContext.init(module.op, &context, observed.allocator(), &cache);
3517 defer ctx.deinit();
3518 typed_analysis_counter = 0;
3519 _ = try MeteredTypedAnalysis.get(&ctx, module.op);
3520 try std.testing.expectEqual(2, observed.allocations);
3521 var another = MeteredTypedAnalysis.descriptor;
3522 another.id = analysisId("another-analysis");
3523 try std.testing.expectError(
3524 error.WorkExhausted,
3525 ctx.getAnalysis(module.op, &another, computeMeteredTyped, cleanupMeteredTyped),
3526 );
3527 try std.testing.expectEqual(1, typed_analysis_counter);
3528 try std.testing.expectEqual(2, observed.allocations);
3529 try std.testing.expectEqual(capacity, cache.entries.capacity());
3530 try std.testing.expectEqual(1, cache.entries.count());
3531 try std.testing.expectEqual(2, ledger.view().charged.analysis_computations);
3532 try std.testing.expectEqual(1, ledger.view().executed.work.analysis_computations);
3533 try std.testing.expectEqual(.exhausted, ledger.view().events[2].outcome);
3534 try std.testing.expectEqual(0, ledger.view().events[2].executed.work.analysis_computations);
3535 }
3536
3537 var metered_pass_runs: u32 = 0;
3538
3539 fn runMeteredPass(ctx: *PassContext) PassResult {
3540 metered_pass_runs += 1;
3541 ctx.preserveAllAnalyses();
3542 return .success;
3543 }
3544
3545 fn meteredPassBounds(_: subject.work.Input) !subject.work.Bounds {
3546 return .{ .work = .{ .structural_visits = 5 } };
3547 }
3548
3549 test "U0 pass admission refuses before the callback and uses the existing failure finalization" {
3550 const revision = @import("../../product/revision/root.zig");
3551 const fixture = @import("../../dialects/fixture/root.zig");
3552 const allocator = std.testing.allocator;
3553 var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
3554 defer context.deinit(allocator);
3555 const module = try fixture.TestDialect.ModuleOp.create(&context, .unknown);
3556 const identity = revision.record.Version{ .name = "u0-metered-pass", .version = 1 };
3557 const ledger = try revision.AccountingV1.create(allocator, .{
3558 .allowance = .{ .structural_visits = 4, .allocation_capacity = 65536 },
3559 .workspace = 4096,
3560 .events = 8,
3561 }, &.{identity});
3562 defer ledger.destroy();
3563 var cache = try AnalysisCache.initAccounted(allocator, null, ledger, .{}, 4);
3564 defer cache.deinit();
3565 var manager = PassManager.init(allocator);
3566 defer manager.deinit();
3567 try manager.addPass(.{
3568 .name = identity.name,
3569 .description = "bounded pass witness",
3570 .run_fn = runMeteredPass,
3571 .work_contract = .{ .identity = identity, .estimate = meteredPassBounds },
3572 });
3573 metered_pass_runs = 0;
3574 try std.testing.expectEqual(
3575 .failure,
3576 manager.runWithAnalysisCache(module.op, &context, &cache, .{}),
3577 );
3578 try std.testing.expectEqual(0, metered_pass_runs);
3579 try std.testing.expectEqual(0, manager.stats.pass_runs);
3580 try std.testing.expectEqual(1, manager.stats.pass_failures);
3581 try std.testing.expectEqual(.exhausted, ledger.view().outcome);
3582 try std.testing.expectEqual(5, ledger.view().charged.structural_visits);
3583 const failure = manager.getLastFailureReproducer() orelse return error.TestExpectedFailure;
3584 try std.testing.expectEqual(.exhausted, failure.failure_kind.?);
3585 const charged = ledger.view().charged;
3586 try std.testing.expectEqual(.failure, manager.runWithAnalysisCache(
3587 module.op,
3588 &context,
3589 &cache,
3590 .{ .max_threads = 2 },
3591 ));
3592 try std.testing.expectEqual(0, metered_pass_runs);
3593 try std.testing.expectEqual(1, manager.stats.pass_failures);
3594 try std.testing.expectEqualDeep(charged, ledger.view().charged);
3595 try std.testing.expectEqual(.exhausted, manager.getLastFailureReproducer().?.failure_kind.?);
3596 }
3597
3598 fn runMeteredAnalysisPass(ctx: *PassContext) PassResult {
3599 metered_pass_runs += 1;
3600 _ = MeteredTypedAnalysis.get(ctx, ctx.op) catch return .failure;
3601 ctx.preserveAllAnalyses();
3602 return .success;
3603 }
3604
3605 fn runSuppressingAnalysisFailure(ctx: *PassContext) PassResult {
3606 metered_pass_runs += 1;
3607 _ = MeteredTypedAnalysis.get(ctx, ctx.op) catch return .success;
3608 return .success;
3609 }
3610
3611 test "U0 pass accounting executes repeated entries and records one cached analysis computation" {
3612 const revision = @import("../../product/revision/root.zig");
3613 const fixture = @import("../../dialects/fixture/root.zig");
3614 const allocator = std.testing.allocator;
3615 var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
3616 defer context.deinit(allocator);
3617 const module = try fixture.TestDialect.ModuleOp.create(&context, .unknown);
3618 const identity = revision.record.Version{ .name = "u0-metered-pass", .version = 1 };
3619 const ledger = try revision.AccountingV1.create(allocator, .{
3620 .allowance = .{ .structural_visits = 15, .analysis_computations = 1, .allocation_capacity = 65536 },
3621 .workspace = 4096,
3622 .events = 8,
3623 }, &.{ identity, identity });
3624 defer ledger.destroy();
3625 var cache = try AnalysisCache.initAccounted(allocator, null, ledger, .{}, 4);
3626 defer cache.deinit();
3627 var manager = PassManager.init(allocator);
3628 defer manager.deinit();
3629 const entry = Pass{
3630 .name = identity.name,
3631 .description = "bounded pass witness",
3632 .run_fn = runMeteredAnalysisPass,
3633 .rerun_policy = .skip_if_unchanged,
3634 .work_contract = .{ .identity = identity, .estimate = meteredPassBounds },
3635 };
3636 try manager.addPass(entry);
3637 try manager.addPass(entry);
3638 metered_pass_runs = 0;
3639 typed_analysis_counter = 0;
3640 try std.testing.expectEqual(.success, manager.runWithAnalysisCache(module.op, &context, &cache, .{}));
3641 const receipt = ledger.view();
3642 try std.testing.expectEqual(2, metered_pass_runs);
3643 try std.testing.expectEqual(1, typed_analysis_counter);
3644 try std.testing.expectEqual(0, manager.stats.passes_skipped);
3645 try std.testing.expectEqual(2, receipt.executed.counters.pass_runs);
3646 try std.testing.expectEqual(1, receipt.executed.counters.analysis_misses);
3647 try std.testing.expectEqual(1, receipt.executed.counters.analysis_hits);
3648 try std.testing.expectEqual(15, receipt.charged.structural_visits);
3649 try std.testing.expectEqual(1, receipt.events[2].parent.?);
3650 try ledger.producersComplete();
3651 }
3652
3653 test "U0 pass propagates analysis exhaustion even when native code reports success" {
3654 const revision = @import("../../product/revision/root.zig");
3655 const fixture = @import("../../dialects/fixture/root.zig");
3656 const allocator = std.testing.allocator;
3657 var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
3658 defer context.deinit(allocator);
3659 const module = try fixture.TestDialect.ModuleOp.create(&context, .unknown);
3660 const callbacks = [_]*const fn (*PassContext) PassResult{
3661 runMeteredAnalysisPass, runSuppressingAnalysisFailure,
3662 };
3663 for (callbacks) |callback| {
3664 const identity = revision.record.Version{ .name = "u0-metered-pass", .version = 1 };
3665 const ledger = try revision.AccountingV1.create(allocator, .{
3666 .allowance = .{ .structural_visits = 9, .analysis_computations = 1, .allocation_capacity = 65536 },
3667 .workspace = 4096,
3668 .events = 8,
3669 }, &.{identity});
3670 defer ledger.destroy();
3671 var cache = try AnalysisCache.initAccounted(allocator, null, ledger, .{}, 4);
3672 defer cache.deinit();
3673 var manager = PassManager.init(allocator);
3674 defer manager.deinit();
3675 try manager.addPass(.{
3676 .name = identity.name,
3677 .description = "bounded pass witness",
3678 .run_fn = callback,
3679 .work_contract = .{ .identity = identity, .estimate = meteredPassBounds },
3680 });
3681 metered_pass_runs = 0;
3682 typed_analysis_counter = 0;
3683 try std.testing.expectEqual(.failure, manager.runWithAnalysisCache(module.op, &context, &cache, .{}));
3684 try std.testing.expectEqual(1, metered_pass_runs);
3685 try std.testing.expectEqual(0, typed_analysis_counter);
3686 try std.testing.expectEqual(1, manager.stats.pass_runs);
3687 try std.testing.expectEqual(1, manager.stats.pass_failures);
3688 try std.testing.expectEqual(1, ledger.view().executed.counters.pass_runs);
3689 try std.testing.expectEqual(.exhausted, ledger.view().events[1].outcome);
3690 const failure = manager.getLastFailureReproducer() orelse return error.TestExpectedFailure;
3691 try std.testing.expectEqual(.exhausted, failure.failure_kind.?);
3692 }
3693 }
3694
3695 test "U0 pass refuses scheduling outside the declared serial job before its callback" {
3696 const revision = @import("../../product/revision/root.zig");
3697 const fixture = @import("../../dialects/fixture/root.zig");
3698 const allocator = std.testing.allocator;
3699 var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
3700 defer context.deinit(allocator);
3701 const module = try fixture.TestDialect.ModuleOp.create(&context, .unknown);
3702 const ledger = try revision.AccountingV1.create(allocator, .{
3703 .allowance = .{ .allocation_capacity = 65536 },
3704 .workspace = 4096,
3705 .events = 8,
3706 }, &.{});
3707 defer ledger.destroy();
3708 var cache = try AnalysisCache.initAccounted(allocator, null, ledger, .{}, 4);
3709 defer cache.deinit();
3710 var manager = PassManager.init(allocator);
3711 defer manager.deinit();
3712 try manager.addPass(.{ .name = "transient", .description = "", .run_fn = runMeteredPass });
3713 metered_pass_runs = 0;
3714 try std.testing.expectEqual(.failure, manager.runWithAnalysisCache(
3715 module.op,
3716 &context,
3717 &cache,
3718 .{ .max_threads = 2 },
3719 ));
3720 try std.testing.expectEqual(0, metered_pass_runs);
3721 try std.testing.expect(ledger.view().missing_work_contract);
3722 try std.testing.expectEqual(.rejected, ledger.view().outcome);
3723 }
3724
3725 test "U0 pass missing contract records successful and failed physical callbacks without qualification" {
3726 const revision = @import("../../product/revision/root.zig");
3727 const fixture = @import("../../dialects/fixture/root.zig");
3728 const allocator = std.testing.allocator;
3729 var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
3730 defer context.deinit(allocator);
3731 const module = try fixture.TestDialect.ModuleOp.create(&context, .unknown);
3732 const callbacks = [_]*const fn (*PassContext) PassResult{
3733 runMeteredPass, runMeteredAnalysisPass,
3734 };
3735 for (callbacks, 0..) |callback, index| {
3736 const ledger = try revision.AccountingV1.create(allocator, .{
3737 .allowance = .{ .allocation_capacity = 65536 },
3738 .workspace = 4096,
3739 .events = 8,
3740 }, &.{});
3741 defer ledger.destroy();
3742 var cache = try AnalysisCache.initAccounted(allocator, null, ledger, .{}, 4);
3743 defer cache.deinit();
3744 var manager = PassManager.init(allocator);
3745 defer manager.deinit();
3746 try manager.addPass(.{ .name = "uncontracted", .description = "", .run_fn = callback });
3747 const result = manager.runWithAnalysisCache(module.op, &context, &cache, .{});
3748 try std.testing.expectEqual(if (index == 0) PassResult.success else .failure, result);
3749 try std.testing.expect(ledger.view().missing_work_contract);
3750 try std.testing.expectEqual(1, manager.stats.pass_runs);
3751 try std.testing.expectEqual(1, ledger.view().executed.counters.pass_runs);
3752 if (index == 0) {
3753 try std.testing.expectError(error.MissingWorkContract, ledger.producersComplete());
3754 } else {
3755 try std.testing.expectEqual(.exhausted, ledger.view().outcome);
3756 try std.testing.expectError(error.WorkExhausted, ledger.producersComplete());
3757 }
3758 }
3759 }
3760
3761 fn runSuppressingContextFailure(ctx: *PassContext) PassResult {
3762 const payload: [1024]u8 = @splat('x');
3763 _ = ctx.ir_ctx.getStringAttr(&payload) catch return .success;
3764 return .success;
3765 }
3766
3767 test "U0 pass caught Context exhaustion stops subsequent callbacks through one finalizer" {
3768 try checkContextPassFailure(false);
3769 try checkContextPassFailure(true);
3770 }
3771
3772 fn checkContextPassFailure(already_exhausted: bool) !void {
3773 const revision = @import("../../product/revision/root.zig");
3774 const fixture = @import("../../dialects/fixture/root.zig");
3775 const allocator = std.testing.allocator;
3776 var limits = ir.Context.Limits.testing;
3777 limits.attributes.payload_bytes = 128;
3778 var context = try ir.Context.init(allocator, limits);
3779 defer context.deinit(allocator);
3780 const module = try fixture.TestDialect.ModuleOp.create(&context, .unknown);
3781 const identity = revision.record.Version{ .name = "u0-context-pass", .version = 1 };
3782 const ledger = try revision.AccountingV1.create(allocator, .{
3783 .allowance = revision.WorkVector.uniform(65536),
3784 .workspace = 4096,
3785 .events = 8,
3786 }, &.{ identity, identity });
3787 defer ledger.destroy();
3788 var cache = try AnalysisCache.initAccounted(
3789 allocator,
3790 null,
3791 ledger,
3792 .{ .context = &context },
3793 4,
3794 );
3795 defer cache.deinit();
3796 var manager = PassManager.init(allocator);
3797 defer manager.deinit();
3798 const contract = subject.work.Contract{ .identity = identity, .estimate = meteredPassBounds };
3799 try manager.addPass(.{
3800 .name = identity.name,
3801 .description = "caught Context storage failure",
3802 .run_fn = runSuppressingContextFailure,
3803 .work_contract = contract,
3804 });
3805 try manager.addPass(.{
3806 .name = identity.name,
3807 .description = "must not execute after Context exhaustion",
3808 .run_fn = runMeteredPass,
3809 .work_contract = contract,
3810 });
3811 if (already_exhausted) {
3812 var ctx = PassContext.init(module.op, &context, allocator, &cache);
3813 defer ctx.deinit();
3814 _ = runSuppressingContextFailure(&ctx);
3815 }
3816 metered_pass_runs = 0;
3817 const result = manager.runWithAnalysisCache(module.op, &context, &cache, .{});
3818 try std.testing.expectEqual(.attribute_payloads, context.exhaustedSegment().?);
3819 try std.testing.expectEqual(.failure, result);
3820 try std.testing.expectEqual(0, metered_pass_runs);
3821 try std.testing.expectEqual(@intFromBool(!already_exhausted), manager.stats.pass_runs);
3822 try std.testing.expectEqual(.exhausted, ledger.view().outcome);
3823 if (!already_exhausted) {
3824 try std.testing.expectEqual(.exhausted, ledger.view().events[1].outcome);
3825 }
3826 try std.testing.expectEqual(.exhausted, manager.getLastFailureReproducer().?.failure_kind.?);
3827 }
3828
3829 fn runSuppressingStorageFailure(ctx: *PassContext) PassResult {
3830 const bytes = ctx.analysis_cache.allocator.alloc(u8, 4096) catch return .success;
3831 ctx.analysis_cache.allocator.free(bytes);
3832 return .success;
3833 }
3834
3835 fn computeSuppressingContextFailure(ctx: *PassContext, op: *ir.Operation) !*anyopaque {
3836 const value = try computeMeteredTyped(ctx, op);
3837 _ = runSuppressingContextFailure(ctx);
3838 return value;
3839 }
3840
3841 fn refreshSuppressingContextFailure(ctx: *PassContext, _: *ir.Operation, _: *anyopaque) !void {
3842 analysis_counter += 1;
3843 _ = runSuppressingContextFailure(ctx);
3844 }
3845
3846 const ContextFailureMode = enum { compute, hit, refresh_before, refresh_during };
3847
3848 test "U0 analysis refuses setup after Context exhaustion without acquiring cache storage" {
3849 const revision = @import("../../product/revision/root.zig");
3850 const allocator = std.testing.allocator;
3851 var limits = ir.Context.Limits.testing;
3852 limits.attributes.payload_bytes = 128;
3853 var context = try ir.Context.init(allocator, limits);
3854 defer context.deinit(allocator);
3855 const payload: [1024]u8 = @splat('x');
3856 try std.testing.expectError(error.OutOfMemory, context.getStringAttr(&payload));
3857 const ledger = try revision.AccountingV1.create(allocator, .{
3858 .allowance = revision.WorkVector.uniform(65536),
3859 .workspace = 4096,
3860 .events = 8,
3861 }, &.{});
3862 defer ledger.destroy();
3863 var failing = std.testing.FailingAllocator.init(allocator, .{ .fail_index = 0 });
3864 const result = AnalysisCache.initAccounted(
3865 failing.allocator(),
3866 null,
3867 ledger,
3868 .{ .context = &context },
3869 4,
3870 );
3871 if (result) |value| {
3872 var cache = value;
3873 cache.deinit();
3874 } else |_| {}
3875 try std.testing.expectError(error.WorkExhausted, result);
3876 try std.testing.expect(!failing.has_induced_failure);
3877 try std.testing.expectEqual(.exhausted, ledger.view().outcome);
3878 try std.testing.expectEqual(0, ledger.view().charged.allocation_capacity);
3879 }
3880
3881 test "U0 analysis caught Context exhaustion refuses computation results hits and refresh" {
3882 for (std.enums.values(ContextFailureMode)) |mode| try checkContextAnalysisFailure(mode);
3883 }
3884
3885 fn checkContextAnalysisFailure(mode: ContextFailureMode) !void {
3886 const revision = @import("../../product/revision/root.zig");
3887 const fixture = @import("../../dialects/fixture/root.zig");
3888 const allocator = std.testing.allocator;
3889 var limits = ir.Context.Limits.testing;
3890 limits.attributes.payload_bytes = 128;
3891 var context = try ir.Context.init(allocator, limits);
3892 defer context.deinit(allocator);
3893 const module = try fixture.TestDialect.ModuleOp.create(&context, .unknown);
3894 const ledger = try revision.AccountingV1.create(allocator, .{
3895 .allowance = revision.WorkVector.uniform(65536),
3896 .workspace = 4096,
3897 .events = 8,
3898 }, &.{});
3899 defer ledger.destroy();
3900 var stats: PassManagerStats = .{};
3901 var cache = try AnalysisCache.initAccounted(
3902 allocator,
3903 &stats,
3904 ledger,
3905 .{ .context = &context },
3906 4,
3907 );
3908 defer cache.deinit();
3909 var ctx = PassContext.init(module.op, &context, allocator, &cache);
3910 defer ctx.deinit();
3911 typed_analysis_counter = 0;
3912 typed_analysis_cleanup_count = 0;
3913 analysis_counter = 0;
3914 const descriptor = &MeteredTypedAnalysis.descriptor;
3915 if (mode != .compute) _ = try MeteredTypedAnalysis.get(&ctx, module.op);
3916 if (mode == .hit or mode == .refresh_before) _ = runSuppressingContextFailure(&ctx);
3917 switch (mode) {
3918 .compute, .hit => try std.testing.expectError(error.WorkExhausted, ctx.getAnalysis(
3919 module.op,
3920 descriptor,
3921 computeSuppressingContextFailure,
3922 cleanupMeteredTyped,
3923 )),
3924 .refresh_before, .refresh_during => try std.testing.expectError(
3925 error.WorkExhausted,
3926 ctx.refreshAnalysis(module.op, descriptor, refreshSuppressingContextFailure),
3927 ),
3928 }
3929 try std.testing.expectEqual(.attribute_payloads, context.exhaustedSegment().?);
3930 try std.testing.expectEqual(.exhausted, ledger.view().outcome);
3931 try std.testing.expectEqual(1, typed_analysis_counter);
3932 try std.testing.expectEqual(@intFromBool(mode == .compute), typed_analysis_cleanup_count);
3933 try std.testing.expectEqual(@intFromBool(mode != .compute), cache.entries.count());
3934 try std.testing.expectEqual(@intFromBool(mode == .refresh_during), analysis_counter);
3935 try std.testing.expectEqual(0, stats.analysis_hits);
3936 try std.testing.expectEqual(@intFromBool(mode != .compute), stats.analysis_misses);
3937 try std.testing.expectError(
3938 error.TerminalWorkOutcome,
3939 MeteredTypedAnalysis.get(&ctx, module.op),
3940 );
3941 try std.testing.expectEqual(1, typed_analysis_counter);
3942 }
3943
3944 test "U0 pass caught workspace exhaustion stops subsequent callbacks through one finalizer" {
3945 const revision = @import("../../product/revision/root.zig");
3946 const fixture = @import("../../dialects/fixture/root.zig");
3947 const fixed = @import("alloc_fixed");
3948 const allocator = std.testing.allocator;
3949 var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
3950 defer context.deinit(allocator);
3951 const module = try fixture.TestDialect.ModuleOp.create(&context, .unknown);
3952 for ([_]bool{ false, true }) |already_exhausted| {
3953 const identity = revision.record.Version{ .name = "u0-storage-pass", .version = 1 };
3954 const ledger = try revision.AccountingV1.create(allocator, .{
3955 .allowance = revision.WorkVector.uniform(65536),
3956 .workspace = 4096,
3957 .events = 8,
3958 }, &.{ identity, identity });
3959 defer ledger.destroy();
3960 var bytes: [4096]u8 align(@alignOf(usize)) = undefined;
3961 var storage = fixed.Tracked.init(&bytes);
3962 var cache = try AnalysisCache.initAccounted(
3963 storage.allocator(),
3964 null,
3965 ledger,
3966 .{ .workspace = &storage.exhausted },
3967 4,
3968 );
3969 defer cache.deinit();
3970 var manager = PassManager.init(allocator);
3971 defer manager.deinit();
3972 const contract = subject.work.Contract{ .identity = identity, .estimate = meteredPassBounds };
3973 try manager.addPass(.{
3974 .name = identity.name,
3975 .description = "caught fixed storage failure",
3976 .run_fn = runSuppressingStorageFailure,
3977 .work_contract = contract,
3978 });
3979 try manager.addPass(.{
3980 .name = identity.name,
3981 .description = "must not execute after exhaustion",
3982 .run_fn = runMeteredPass,
3983 .work_contract = contract,
3984 });
3985 if (already_exhausted) {
3986 try std.testing.expectError(error.OutOfMemory, storage.allocator().alloc(u8, 4096));
3987 }
3988 metered_pass_runs = 0;
3989 try std.testing.expectEqual(
3990 .failure,
3991 manager.runWithAnalysisCache(module.op, &context, &cache, .{}),
3992 );
3993 try std.testing.expect(storage.exhausted);
3994 try std.testing.expectEqual(0, metered_pass_runs);
3995 try std.testing.expectEqual(@intFromBool(!already_exhausted), manager.stats.pass_runs);
3996 try std.testing.expectEqual(1, manager.stats.pass_failures);
3997 try std.testing.expectEqual(.exhausted, ledger.view().outcome);
3998 try std.testing.expectEqual(.exhausted, manager.getLastFailureReproducer().?.failure_kind.?);
3999 if (!already_exhausted) {
4000 try std.testing.expectEqual(.exhausted, ledger.view().events[1].outcome);
4001 }
4002 }
4003 }
4004
4005 fn computeSuppressingStorageFailure(ctx: *PassContext, op: *ir.Operation) !*anyopaque {
4006 const value = try computeMeteredTyped(ctx, op);
4007 _ = runSuppressingStorageFailure(ctx);
4008 return value;
4009 }
4010
4011 fn refreshSuppressingStorageFailure(ctx: *PassContext, _: *ir.Operation, _: *anyopaque) !void {
4012 analysis_counter += 1;
4013 _ = runSuppressingStorageFailure(ctx);
4014 }
4015
4016 test "U0 analysis caught workspace exhaustion refuses computation results hits and refresh" {
4017 const revision = @import("../../product/revision/root.zig");
4018 const fixture = @import("../../dialects/fixture/root.zig");
4019 const fixed = @import("alloc_fixed");
4020 const allocator = std.testing.allocator;
4021 var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
4022 defer context.deinit(allocator);
4023 const module = try fixture.TestDialect.ModuleOp.create(&context, .unknown);
4024 const Mode = enum { compute, hit, refresh_before, refresh_during };
4025 for (std.enums.values(Mode)) |mode| {
4026 const ledger = try revision.AccountingV1.create(allocator, .{
4027 .allowance = revision.WorkVector.uniform(65536),
4028 .workspace = 4096,
4029 .events = 8,
4030 }, &.{});
4031 defer ledger.destroy();
4032 var bytes: [4096]u8 align(@alignOf(usize)) = undefined;
4033 var storage = fixed.Tracked.init(&bytes);
4034 var stats: PassManagerStats = .{};
4035 var cache = try AnalysisCache.initAccounted(
4036 storage.allocator(),
4037 &stats,
4038 ledger,
4039 .{ .workspace = &storage.exhausted },
4040 4,
4041 );
4042 defer cache.deinit();
4043 var ctx = PassContext.init(module.op, &context, storage.allocator(), &cache);
4044 defer ctx.deinit();
4045 typed_analysis_counter = 0;
4046 typed_analysis_cleanup_count = 0;
4047 analysis_counter = 0;
4048 const descriptor = &MeteredTypedAnalysis.descriptor;
4049 if (mode != .compute) _ = try MeteredTypedAnalysis.get(&ctx, module.op);
4050 if (mode == .hit or mode == .refresh_before) _ = runSuppressingStorageFailure(&ctx);
4051 switch (mode) {
4052 .compute, .hit => try std.testing.expectError(error.WorkExhausted, ctx.getAnalysis(
4053 module.op,
4054 descriptor,
4055 computeSuppressingStorageFailure,
4056 cleanupMeteredTyped,
4057 )),
4058 .refresh_before, .refresh_during => try std.testing.expectError(
4059 error.WorkExhausted,
4060 ctx.refreshAnalysis(module.op, descriptor, refreshSuppressingStorageFailure),
4061 ),
4062 }
4063 try std.testing.expect(storage.exhausted);
4064 try std.testing.expectEqual(.exhausted, ledger.view().outcome);
4065 try std.testing.expectEqual(1, typed_analysis_counter);
4066 try std.testing.expectEqual(@intFromBool(mode == .compute), typed_analysis_cleanup_count);
4067 try std.testing.expectEqual(@intFromBool(mode != .compute), cache.entries.count());
4068 try std.testing.expectEqual(@intFromBool(mode == .refresh_during), analysis_counter);
4069 try std.testing.expectEqual(0, stats.analysis_hits);
4070 try std.testing.expectEqual(@intFromBool(mode != .compute), stats.analysis_misses);
4071 try std.testing.expectError(
4072 error.TerminalWorkOutcome,
4073 MeteredTypedAnalysis.get(&ctx, module.op),
4074 );
4075 try std.testing.expectEqual(1, typed_analysis_counter);
4076 }
4077 }
4078
4079 test "U0 analysis host OOM before workspace allocation stays rejected" {
4080 const revision = @import("../../product/revision/root.zig");
4081 const fixed = @import("alloc_fixed");
4082 const allocator = std.testing.allocator;
4083 const ledger = try revision.AccountingV1.create(allocator, .{
4084 .allowance = .{ .allocation_capacity = 65536 },
4085 .workspace = 4096,
4086 .events = 8,
4087 }, &.{});
4088 defer ledger.destroy();
4089 var bytes: [4096]u8 align(@alignOf(usize)) = undefined;
4090 var storage = fixed.Tracked.init(&bytes);
4091 var failing = std.testing.FailingAllocator.init(storage.allocator(), .{ .fail_index = 0 });
4092 const result = AnalysisCache.initAccounted(
4093 failing.allocator(),
4094 null,
4095 ledger,
4096 .{ .workspace = &storage.exhausted },
4097 4,
4098 );
4099 if (result) |value| {
4100 var cache = value;
4101 cache.deinit();
4102 } else |_| {}
4103 try std.testing.expect(failing.has_induced_failure);
4104 try std.testing.expect(!storage.exhausted);
4105 try std.testing.expectError(error.OutOfMemory, result);
4106 try std.testing.expectEqual(.rejected, ledger.view().outcome);
4107 try std.testing.expect(ledger.view().charged.allocation_capacity > 0);
4108 try std.testing.expectEqual(0, ledger.view().executed.work.analysis_computations);
4109 }
4110
4111 fn allocateWorkerByte(ctx: *PassContext) !void {
4112 analysis_counter += 1;
4113 const allocator = ctx.workerAllocator();
4114 const bytes = try allocator.alloc(u8, 1);
4115 defer allocator.free(bytes);
4116 }
4117
4118 fn computeWithWorkerAllocation(ctx: *PassContext, op: *ir.Operation) !*anyopaque {
4119 try allocateWorkerByte(ctx);
4120 return computeMeteredTyped(ctx, op);
4121 }
4122
4123 fn refreshWithWorkerAllocation(ctx: *PassContext, _: *ir.Operation, value: *anyopaque) !void {
4124 try allocateWorkerByte(ctx);
4125 const typed: *u32 = @ptrCast(@alignCast(value));
4126 typed.* += 1;
4127 }
4128
4129 test "U0 analysis separate worker host OOM rejects computation without retry" {
4130 try checkWorkerAllocationFailure(false, false);
4131 }
4132
4133 test "U0 analysis separate worker host OOM rejects refresh without retry" {
4134 try checkWorkerAllocationFailure(true, false);
4135 }
4136
4137 test "U0 analysis caught workspace exhaustion dominates a later compute error" {
4138 try checkWorkerAllocationFailure(false, true);
4139 }
4140
4141 test "U0 analysis caught workspace exhaustion dominates a later refresh error" {
4142 try checkWorkerAllocationFailure(true, true);
4143 }
4144
4145 fn computeAfterWorkspaceExhaustion(ctx: *PassContext, _: *ir.Operation) !*anyopaque {
4146 analysis_counter += 1;
4147 _ = runSuppressingStorageFailure(ctx);
4148 return error.UnsupportedOperation;
4149 }
4150
4151 fn refreshAfterWorkspaceExhaustion(ctx: *PassContext, _: *ir.Operation, _: *anyopaque) !void {
4152 analysis_counter += 1;
4153 _ = runSuppressingStorageFailure(ctx);
4154 return error.UnsupportedOperation;
4155 }
4156
4157 fn checkWorkerAllocationFailure(refresh: bool, exhaust_workspace: bool) !void {
4158 const revision = @import("../../product/revision/root.zig");
4159 const fixture = @import("../../dialects/fixture/root.zig");
4160 const fixed = @import("alloc_fixed");
4161 const allocator = std.testing.allocator;
4162 var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
4163 defer context.deinit(allocator);
4164 const module = try fixture.TestDialect.ModuleOp.create(&context, .unknown);
4165 const ledger = try revision.AccountingV1.create(allocator, .{
4166 .allowance = revision.WorkVector.uniform(65536),
4167 .workspace = 4096,
4168 .events = 8,
4169 }, &.{});
4170 defer ledger.destroy();
4171 var bytes: [4096]u8 align(@alignOf(usize)) = undefined;
4172 var storage = fixed.Tracked.init(&bytes);
4173 var stats: PassManagerStats = .{};
4174 var cache = try AnalysisCache.initAccounted(
4175 storage.allocator(),
4176 &stats,
4177 ledger,
4178 .{ .workspace = &storage.exhausted },
4179 4,
4180 );
4181 defer cache.deinit();
4182 var failing = std.testing.FailingAllocator.init(allocator, .{ .fail_index = 0 });
4183 var ctx = PassContext.initWithOptions(module.op, &context, storage.allocator(), &cache, .{
4184 .worker_allocator = failing.allocator(),
4185 });
4186 defer ctx.deinit();
4187 typed_analysis_counter = 0;
4188 analysis_counter = 0;
4189 const value = if (refresh) try MeteredTypedAnalysis.get(&ctx, module.op) else null;
4190 const descriptor = &MeteredTypedAnalysis.descriptor;
4191 const expected = if (exhaust_workspace) error.WorkExhausted else error.OutOfMemory;
4192 if (refresh) {
4193 try std.testing.expectError(expected, ctx.refreshAnalysis(
4194 module.op,
4195 descriptor,
4196 if (exhaust_workspace) refreshAfterWorkspaceExhaustion else refreshWithWorkerAllocation,
4197 ));
4198 try std.testing.expectEqual(@as(u32, 1), value.?.*);
4199 } else {
4200 try std.testing.expectError(expected, ctx.getAnalysis(
4201 module.op,
4202 descriptor,
4203 if (exhaust_workspace) computeAfterWorkspaceExhaustion else computeWithWorkerAllocation,
4204 cleanupMeteredTyped,
4205 ));
4206 }
4207 try std.testing.expectEqual(!exhaust_workspace, failing.has_induced_failure);
4208 try std.testing.expectEqual(exhaust_workspace, storage.exhausted);
4209 try std.testing.expectEqual(
4210 if (exhaust_workspace) @as(@TypeOf(ledger.view().outcome), .exhausted) else .rejected,
4211 ledger.view().outcome,
4212 );
4213 try std.testing.expectEqual(@intFromBool(refresh), cache.entries.count());
4214 try std.testing.expectEqual(@intFromBool(refresh), stats.analysis_misses);
4215 try std.testing.expectEqual(
4216 1 + @as(u64, @intFromBool(refresh)),
4217 ledger.view().charged.analysis_computations,
4218 );
4219 try std.testing.expectError(
4220 error.TerminalWorkOutcome,
4221 MeteredTypedAnalysis.get(&ctx, module.op),
4222 );
4223 try std.testing.expectEqual(1, analysis_counter);
4224 }