lib/choir/src/passes/cse/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const ir = @import("../../core/root.zig");
3 const dialects = @import("../../dialects/root.zig");
4 const test_dialect = @import("../../dialects/fixture/root.zig");
5 const passes = @import("../root.zig");
6 const pass_mod = @import("../pass/root.zig");
7 const instrumentation = passes.instrumentation;
8 const cse = @import("root.zig");
9
10 const common_subexpression_elimination_pass_name =
11 cse.common_subexpression_elimination_pass_name;
12 const createCommonSubexpressionEliminationPass =
13 cse.createCommonSubexpressionEliminationPass;
14 const run = cse.run;
15 const PassResult = pass_mod.PassResult;
16
17 const testing = std.testing;
18 const arith = dialects.ArithDialect;
19
20 fn buildTestContext(allocator: std.mem.Allocator) !ir.Context {
21 var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
22 errdefer ctx.deinit(allocator);
23 try test_dialect.registerTestDialect(&ctx);
24 try ir.dialects.loadDialectSpec(&ctx, dialects.arith.spec);
25 return ctx;
26 }
27
28 fn runCsePass(allocator: std.mem.Allocator, module: *ir.Operation, ctx: *ir.Context) !pass_mod.PassManager {
29 var pm = pass_mod.PassManager.init(allocator);
30 errdefer pm.deinit();
31 try pm.addPass(createCommonSubexpressionEliminationPass());
32 try testing.expectEqual(PassResult.success, pm.run(module, ctx));
33 return pm;
34 }
35
36 fn createBinary(
37 ctx: *ir.Context,
38 block: *ir.Block,
39 lhs: *ir.Value,
40 rhs: *ir.Value,
41 ) !arith.AddOp {
42 const op = try arith.AddOp.create(ctx, ir.Location.getUnknown(), lhs, rhs);
43 try block.addOperation(op.op);
44 return op;
45 }
46
47 fn createBranch(ctx: *ir.Context, block: *ir.Block, successors: []const *ir.Block) !*ir.Operation {
48 var builder = ir.OperationBuilder.init(ctx);
49 var state = ir.Operation.State.init("test.br", ir.Location.getUnknown());
50 state.addSuccessors(successors);
51 const op = try builder.create(state);
52 try block.addOperation(op);
53 return op;
54 }
55
56 fn createReturn(ctx: *ir.Context, block: *ir.Block, operands: []const *ir.Value) !test_dialect.TestDialect.ReturnOp {
57 const op = try test_dialect.TestDialect.ReturnOp.create(ctx, ir.Location.getUnknown(), operands);
58 try block.addOperation(op.op);
59 return op;
60 }
61
62 fn registerEffectfulValueOp(ctx: *ir.Context) !void {
63 try ctx.registerOperationInterface(
64 "test.effectful_value",
65 ir.interfaces.EffectOpInterface.entryFor(.{}),
66 );
67 }
68
69 fn createEffectfulValue(
70 ctx: *ir.Context,
71 block: *ir.Block,
72 lhs: *ir.Value,
73 rhs: *ir.Value,
74 ) !*ir.Operation {
75 var builder = ir.OperationBuilder.init(ctx);
76 var state = ir.Operation.State.init("test.effectful_value", ir.Location.getUnknown());
77 state.addOperands(&.{ lhs, rhs });
78 state.addTypes(&.{lhs.type});
79 const op = try builder.create(state);
80 try block.addOperation(op);
81 return op;
82 }
83
84 fn createResultOp(ctx: *ir.Context, block: *ir.Block, name: []const u8, result_type: ir.Type) !*ir.Operation {
85 var builder = ir.OperationBuilder.init(ctx);
86 var state = ir.Operation.State.init(name, ir.Location.getUnknown());
87 state.addTypes(&.{result_type});
88 const op = try builder.create(state);
89 if (std.mem.eql(u8, name, "arith.constant")) try op.setAttr(
90 "value",
91 try arith.getIntAttr(ctx, 42),
92 );
93 try block.addOperation(op);
94 return op;
95 }
96
97 fn cseAttributeValueIncludesKeyAttr(_: *const anyopaque, name: []const u8, _: ir.Attribute) bool {
98 return !std.mem.eql(u8, name, "test.debug_span");
99 }
100
101 fn cseKeyPolicyIncludesKeyAttr(_: *const anyopaque, _: []const u8, _: ir.Attribute) bool {
102 return true;
103 }
104
105 fn cseKeyPolicyCommutesOperands(_: *const anyopaque) bool {
106 return true;
107 }
108
109 fn registerCseAttributeValueOp(ctx: *ir.Context) !void {
110 _ = try ctx.registerOperation("arith.constant", .{});
111 try ctx.registerOperationInterface(
112 "arith.constant",
113 ir.interfaces.CseOpInterface.entryFor(cseAttributeValueIncludesKeyAttr),
114 );
115 }
116
117 fn registerCseKeyPolicyBinaryOp(ctx: *ir.Context) !void {
118 ctx.lookupOperation("arith.add").?.traits.is_commutative = false;
119 try ctx.registerOperationInterface(
120 "arith.add",
121 ir.interfaces.CseOpInterface.keyPolicyEntryFor(
122 cseKeyPolicyIncludesKeyAttr,
123 cseKeyPolicyCommutesOperands,
124 ),
125 );
126 }
127
128 fn createCseAttributeValue(
129 ctx: *ir.Context,
130 block: *ir.Block,
131 result_type: ir.Type,
132 debug_span: i64,
133 ) !*ir.Operation {
134 var builder = ir.OperationBuilder.init(ctx);
135 var state = ir.Operation.State.init("arith.constant", ir.Location.getUnknown());
136 state.addTypes(&.{result_type});
137 const op = try builder.create(state);
138 try op.setAttr("value", try arith.getIntAttr(ctx, 42));
139 try op.setAttr("test.debug_span", try ctx.getI64Attr(debug_span));
140 try block.addOperation(op);
141 return op;
142 }
143
144 fn createCseKeyPolicyBinary(
145 ctx: *ir.Context,
146 block: *ir.Block,
147 lhs: *ir.Value,
148 rhs: *ir.Value,
149 ) !*ir.Operation {
150 var builder = ir.OperationBuilder.init(ctx);
151 var state = ir.Operation.State.init("arith.add", ir.Location.getUnknown());
152 state.addOperands(&.{ lhs, rhs });
153 state.addTypes(&.{lhs.type});
154 const op = try builder.create(state);
155 try block.addOperation(op);
156 return op;
157 }
158
159 test "choir-cse eliminates qualified wrapping-add block-local duplicates" {
160 const allocator = testing.allocator;
161
162 var ctx = try buildTestContext(allocator);
163 defer ctx.deinit(allocator);
164
165 const module = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
166 const block = module.getBodyBlock();
167 const i32_type = try arith.getScalarType(&ctx, .i32);
168 const lhs = try block.addArgument(i32_type, ir.Location.getUnknown());
169 const rhs = try block.addArgument(i32_type, ir.Location.getUnknown());
170
171 const first = try createBinary(&ctx, block, lhs, rhs);
172 const second = try createBinary(&ctx, block, lhs, rhs);
173 const ret = try createReturn(&ctx, block, &.{second.getResult()});
174
175 var statistics = instrumentation.PassStatisticsInstrumentation.init(allocator);
176 defer statistics.deinit();
177
178 var pm = pass_mod.PassManager.init(allocator);
179 defer pm.deinit();
180 try pm.addInstrumentation(statistics.instrumentation());
181 try pm.addPass(createCommonSubexpressionEliminationPass());
182 try testing.expectEqual(PassResult.success, pm.run(module.op, &ctx));
183
184 try testing.expectEqual(@as(usize, 1), ir.inspection.countOperationsNamed(module.op, arith.AddOp.operation_name));
185 try testing.expectEqual(first.getResult(), ret.op.getOperand(0).?);
186 try testing.expectEqual(@as(u64, 1), pm.stats.passes_modified);
187 try testing.expectEqual(@as(u64, 0), pm.stats.analysis_hits);
188 try testing.expectEqual(@as(u64, 0), pm.stats.analysis_misses);
189 try testing.expectEqual(@as(u64, 0), pm.stats.analyses_invalidated);
190 try testing.expectEqual(@as(u64, 1), statistics.get(common_subexpression_elimination_pass_name, "replacements").?);
191 }
192
193 test "choir-cse allocates only its exact workspace before mutation" {
194 comptime {
195 @stardustClaim(
196 @import("alloc_phase").capacity.witness(@import("./root.zig").Workspace, "cse_steady_no_alloc"),
197 null,
198 null,
199 null,
200 null,
201 null,
202 null,
203 );
204 }
205
206 const allocator = testing.allocator;
207
208 var ctx = try buildTestContext(allocator);
209 defer ctx.deinit(allocator);
210 _ = try ctx.registerOperation("arith.add", .{});
211
212 const module = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
213 const block = module.getBodyBlock();
214 const i32_type = try arith.getScalarType(&ctx, .i32);
215 const lhs = try block.addArgument(i32_type, ir.Location.getUnknown());
216 const rhs = try block.addArgument(i32_type, ir.Location.getUnknown());
217
218 var first_state = ir.Operation.State.init("arith.add", ir.Location.getUnknown());
219 first_state.addOperands(&.{ lhs, rhs });
220 first_state.addTypes(&.{i32_type});
221 const first = try ctx.createOperation(first_state);
222 try block.addOperation(first);
223
224 var second_state = ir.Operation.State.init("arith.add", ir.Location.getUnknown());
225 second_state.addOperands(&.{ lhs, rhs });
226 second_state.addTypes(&.{i32_type});
227 const second = try ctx.createOperation(second_state);
228 try block.addOperation(second);
229 const ret = try createReturn(
230 &ctx,
231 block,
232 &.{second.getResult(0).?},
233 );
234
235 var failing = std.testing.FailingAllocator.init(allocator, .{ .fail_index = 0 });
236 var analysis_cache = pass_mod.AnalysisCache.init(failing.allocator(), null);
237 defer analysis_cache.deinit();
238 var pass_ctx = pass_mod.PassContext.init(
239 module.op,
240 &ctx,
241 failing.allocator(),
242 &analysis_cache,
243 );
244 defer pass_ctx.deinit();
245
246 try testing.expectEqual(PassResult.success, run(&pass_ctx));
247 try testing.expectEqual(@as(usize, 0), failing.alloc_index);
248 try testing.expectEqual(
249 @as(usize, 1),
250 ir.inspection.countOperationsNamed(
251 module.op,
252 "arith.add",
253 ),
254 );
255 try testing.expectEqual(first.getResult(0).?, ret.op.getOperand(0).?);
256 }
257
258 test "choir-cse isolates nested region candidates while reusing scoped slots" {
259 const allocator = testing.allocator;
260
261 var ctx = try buildTestContext(allocator);
262 defer ctx.deinit(allocator);
263 _ = try ctx.registerOperation("arith.constant", .{});
264 _ = try ctx.registerOperation("test.cse_scope", .{});
265
266 const module = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
267 const block = module.getBodyBlock();
268 const i32_type = try arith.getScalarType(&ctx, .i32);
269 const first = try createResultOp(&ctx, block, "arith.constant", i32_type);
270
271 var scope_state = ir.Operation.State.init("test.cse_scope", ir.Location.getUnknown());
272 scope_state.addRegion();
273 const scope = try ctx.createOperation(scope_state);
274 try block.addOperation(scope);
275 const nested_block = try scope.getRegion(0).?.addBlock();
276 const nested = try createResultOp(
277 &ctx,
278 nested_block,
279 "arith.constant",
280 i32_type,
281 );
282
283 const second = try createResultOp(&ctx, block, "arith.constant", i32_type);
284 const ret = try createReturn(&ctx, block, &.{second.getResult(0).?});
285
286 var pm = try runCsePass(allocator, module.op, &ctx);
287 defer pm.deinit();
288
289 try testing.expectEqual(
290 @as(usize, 2),
291 ir.inspection.countOperationsNamed(module.op, "arith.constant"),
292 );
293 try testing.expectEqual(nested_block, nested.parent_block.?);
294 try testing.expectEqual(first.getResult(0).?, ret.op.getOperand(0).?);
295 try testing.expectEqual(@as(u64, 1), pm.stats.passes_modified);
296 }
297
298 test "choir-cse lets ops exclude nonsemantic attributes" {
299 const allocator = testing.allocator;
300
301 var ctx = try buildTestContext(allocator);
302 defer ctx.deinit(allocator);
303 try registerCseAttributeValueOp(&ctx);
304
305 const module = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
306 const block = module.getBodyBlock();
307 const i32_type = try arith.getScalarType(&ctx, .i32);
308
309 const first = try createCseAttributeValue(&ctx, block, i32_type, 10);
310 const second = try createCseAttributeValue(&ctx, block, i32_type, 20);
311 const ret = try createReturn(&ctx, block, &.{second.getResult(0).?});
312
313 var pm = try runCsePass(allocator, module.op, &ctx);
314 defer pm.deinit();
315
316 try testing.expectEqual(
317 @as(usize, 1),
318 ir.inspection.countOperationsNamed(module.op, "arith.constant"),
319 );
320 try testing.expectEqual(first.getResult(0).?, ret.op.getOperand(0).?);
321 try testing.expectEqual(@as(u64, 1), pm.stats.passes_modified);
322 }
323
324 test "choir-cse keeps different interned attribute values distinct" {
325 const allocator = testing.allocator;
326
327 var ctx = try buildTestContext(allocator);
328 defer ctx.deinit(allocator);
329 _ = try ctx.registerOperation("arith.constant", .{});
330
331 const module = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
332 const block = module.getBodyBlock();
333 const i32_type = try arith.getScalarType(&ctx, .i32);
334
335 const first = try createResultOp(&ctx, block, "arith.constant", i32_type);
336 try first.setAttr("test.value", try ctx.getI64Attr(1));
337 const second = try createResultOp(&ctx, block, "arith.constant", i32_type);
338 try second.setAttr("test.value", try ctx.getI64Attr(2));
339 const ret = try createReturn(&ctx, block, &.{second.getResult(0).?});
340
341 var pm = try runCsePass(allocator, module.op, &ctx);
342 defer pm.deinit();
343
344 try testing.expectEqual(
345 @as(usize, 2),
346 ir.inspection.countOperationsNamed(module.op, "arith.constant"),
347 );
348 try testing.expectEqual(second.getResult(0).?, ret.op.getOperand(0).?);
349 try testing.expectEqual(@as(u64, 0), pm.stats.passes_modified);
350 }
351
352 test "choir-cse lets op key policy commute operands" {
353 const allocator = testing.allocator;
354
355 var ctx = try buildTestContext(allocator);
356 defer ctx.deinit(allocator);
357 try registerCseKeyPolicyBinaryOp(&ctx);
358
359 const module = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
360 const block = module.getBodyBlock();
361 const i32_type = try arith.getScalarType(&ctx, .i32);
362 const lhs = try block.addArgument(i32_type, ir.Location.getUnknown());
363 const rhs = try block.addArgument(i32_type, ir.Location.getUnknown());
364
365 const first = try createCseKeyPolicyBinary(&ctx, block, lhs, rhs);
366 const second = try createCseKeyPolicyBinary(&ctx, block, rhs, lhs);
367 const ret = try createReturn(&ctx, block, &.{second.getResult(0).?});
368
369 var pm = try runCsePass(allocator, module.op, &ctx);
370 defer pm.deinit();
371
372 try testing.expectEqual(
373 @as(usize, 1),
374 ir.inspection.countOperationsNamed(module.op, "arith.add"),
375 );
376 try testing.expectEqual(first.getResult(0).?, ret.op.getOperand(0).?);
377 try testing.expectEqual(@as(u64, 1), pm.stats.passes_modified);
378 }
379
380 test "choir-cse keys operands after earlier replacements" {
381 const allocator = testing.allocator;
382
383 var ctx = try buildTestContext(allocator);
384 defer ctx.deinit(allocator);
385 _ = try ctx.registerOperation("arith.constant", .{});
386
387 const module = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
388 const block = module.getBodyBlock();
389 const i32_type = try arith.getScalarType(&ctx, .i32);
390 const seed = try block.addArgument(i32_type, ir.Location.getUnknown());
391
392 const first_leaf = try createResultOp(&ctx, block, "arith.constant", i32_type);
393 const first = try createBinary(&ctx, block, seed, first_leaf.getResult(0).?);
394 const second_leaf = try createResultOp(&ctx, block, "arith.constant", i32_type);
395 const second = try createBinary(&ctx, block, seed, second_leaf.getResult(0).?);
396 const ret = try createReturn(&ctx, block, &.{second.getResult()});
397
398 var pm = try runCsePass(allocator, module.op, &ctx);
399 defer pm.deinit();
400
401 try testing.expectEqual(
402 @as(usize, 1),
403 ir.inspection.countOperationsNamed(module.op, "arith.constant"),
404 );
405 try testing.expectEqual(@as(usize, 1), ir.inspection.countOperationsNamed(module.op, arith.AddOp.operation_name));
406 try testing.expectEqual(first.getResult(), ret.op.getOperand(0).?);
407 try testing.expectEqual(@as(u64, 1), pm.stats.passes_modified);
408 }
409
410 test "choir-cse uses commutative traits for arith add" {
411 const allocator = testing.allocator;
412
413 var ctx = try buildTestContext(allocator);
414 defer ctx.deinit(allocator);
415 try ir.dialects.loadDialectSpec(&ctx, dialects.arith.spec);
416
417 const module = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
418 const block = module.getBodyBlock();
419 const i32_type = try arith.getScalarType(&ctx, .i32);
420 const lhs = try block.addArgument(i32_type, ir.Location.getUnknown());
421 const rhs = try block.addArgument(i32_type, ir.Location.getUnknown());
422
423 const first = try arith.AddOp.create(&ctx, ir.Location.getUnknown(), lhs, rhs);
424 try block.addOperation(first.op);
425 const second = try arith.AddOp.create(&ctx, ir.Location.getUnknown(), rhs, lhs);
426 try block.addOperation(second.op);
427 const ret = try createReturn(&ctx, block, &.{second.getResult()});
428
429 var pm = try runCsePass(allocator, module.op, &ctx);
430 defer pm.deinit();
431
432 try testing.expectEqual(@as(usize, 1), ir.inspection.countOperationsNamed(module.op, arith.AddOp.operation_name));
433 try testing.expectEqual(first.getResult(), ret.op.getOperand(0).?);
434 try testing.expectEqual(@as(u64, 1), pm.stats.passes_modified);
435 }
436
437 test "choir-cse eliminates duplicates dominated by an earlier block" {
438 const allocator = testing.allocator;
439
440 var ctx = try buildTestContext(allocator);
441 defer ctx.deinit(allocator);
442
443 const module = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
444 const region = module.getBody();
445 const entry = module.getBodyBlock();
446 const merge = try region.addBlock();
447 const i32_type = try arith.getScalarType(&ctx, .i32);
448 const lhs = try entry.addArgument(i32_type, ir.Location.getUnknown());
449 const rhs = try entry.addArgument(i32_type, ir.Location.getUnknown());
450
451 const first = try createBinary(&ctx, entry, lhs, rhs);
452 _ = try createBranch(&ctx, entry, &.{merge});
453 const second = try createBinary(&ctx, merge, lhs, rhs);
454 const ret = try createReturn(&ctx, merge, &.{second.getResult()});
455
456 var pm = try runCsePass(allocator, module.op, &ctx);
457 defer pm.deinit();
458
459 try testing.expectEqual(
460 @as(usize, 1),
461 ir.inspection.countOperationsNamed(
462 module.op,
463 arith.AddOp.operation_name,
464 ),
465 );
466 try testing.expectEqual(first.getResult(), ret.op.getOperand(0).?);
467 try testing.expectEqual(@as(u64, 0), pm.stats.analysis_hits);
468 try testing.expectEqual(@as(u64, 2), pm.stats.analysis_misses);
469 }
470
471 test "choir-cse skips dominance when candidates occupy one of several blocks" {
472 const allocator = testing.allocator;
473
474 var ctx = try buildTestContext(allocator);
475 defer ctx.deinit(allocator);
476
477 const module = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
478 const region = module.getBody();
479 const entry = module.getBodyBlock();
480 const merge = try region.addBlock();
481 const i32_type = try arith.getScalarType(&ctx, .i32);
482 const lhs = try entry.addArgument(i32_type, ir.Location.getUnknown());
483 const rhs = try entry.addArgument(i32_type, ir.Location.getUnknown());
484
485 const first = try createBinary(&ctx, entry, lhs, rhs);
486 const second = try createBinary(&ctx, entry, lhs, rhs);
487 _ = try createBranch(&ctx, entry, &.{merge});
488 const ret = try createReturn(&ctx, merge, &.{second.getResult()});
489
490 var pm = try runCsePass(allocator, module.op, &ctx);
491 defer pm.deinit();
492
493 try testing.expectEqual(
494 @as(usize, 1),
495 ir.inspection.countOperationsNamed(
496 module.op,
497 arith.AddOp.operation_name,
498 ),
499 );
500 try testing.expectEqual(first.getResult(), ret.op.getOperand(0).?);
501 try testing.expectEqual(@as(u64, 0), pm.stats.analysis_hits);
502 try testing.expectEqual(@as(u64, 0), pm.stats.analysis_misses);
503 }
504
505 test "choir-cse keeps equal branch values when neither dominates the other" {
506 const allocator = testing.allocator;
507
508 var ctx = try buildTestContext(allocator);
509 defer ctx.deinit(allocator);
510
511 const module = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
512 const region = module.getBody();
513 const entry = module.getBodyBlock();
514 const then_block = try region.addBlock();
515 const else_block = try region.addBlock();
516 const merge = try region.addBlock();
517 const i32_type = try arith.getScalarType(&ctx, .i32);
518 const lhs = try entry.addArgument(i32_type, ir.Location.getUnknown());
519 const rhs = try entry.addArgument(i32_type, ir.Location.getUnknown());
520
521 _ = try createBranch(&ctx, entry, &.{ then_block, else_block });
522 _ = try createBinary(&ctx, then_block, lhs, rhs);
523 _ = try createBranch(&ctx, then_block, &.{merge});
524 _ = try createBinary(&ctx, else_block, lhs, rhs);
525 _ = try createBranch(&ctx, else_block, &.{merge});
526 _ = try createReturn(&ctx, merge, &.{});
527
528 var pm = try runCsePass(allocator, module.op, &ctx);
529 defer pm.deinit();
530
531 try testing.expectEqual(
532 @as(usize, 2),
533 ir.inspection.countOperationsNamed(
534 module.op,
535 arith.AddOp.operation_name,
536 ),
537 );
538 try testing.expectEqual(@as(u64, 0), pm.stats.passes_modified);
539 try testing.expectEqual(@as(u64, 0), pm.stats.analysis_hits);
540 try testing.expectEqual(@as(u64, 2), pm.stats.analysis_misses);
541 }
542
543 test "choir-cse keeps effectful duplicates" {
544 const allocator = testing.allocator;
545
546 var ctx = try buildTestContext(allocator);
547 defer ctx.deinit(allocator);
548 try registerEffectfulValueOp(&ctx);
549
550 const module = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
551 const block = module.getBodyBlock();
552 const i32_type = try arith.getScalarType(&ctx, .i32);
553 const lhs = try block.addArgument(i32_type, ir.Location.getUnknown());
554 const rhs = try block.addArgument(i32_type, ir.Location.getUnknown());
555
556 _ = try createEffectfulValue(&ctx, block, lhs, rhs);
557 const second = try createEffectfulValue(&ctx, block, lhs, rhs);
558 const ret = try createReturn(&ctx, block, &.{second.getResult(0).?});
559
560 var pm = try runCsePass(allocator, module.op, &ctx);
561 defer pm.deinit();
562
563 try testing.expectEqual(@as(usize, 2), ir.inspection.countOperationsNamed(module.op, "test.effectful_value"));
564 try testing.expectEqual(second.getResult(0).?, ret.op.getOperand(0).?);
565 try testing.expectEqual(@as(u64, 0), pm.stats.passes_modified);
566 }