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 }