lib/choir/src/dialects/scf.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const alloc_arena = @import("alloc_arena");
   3 const ir = @import("../core/root.zig");
   4 const effects = ir.interfaces.effects;
   5 
   6 pub const ScfDialect = struct {
   7     pub const name = "scf";
   8     const op_templates = ir.dialects.operationTemplate.dialect(@This());
   9     pub const spec = ir.dialects.dialectSpec(@This(), .{
  10         .op_interface_fallbacks = &.{
  11             .{ .id = ir.interfaces.Evaluatable.id, .fallback = ScfEval.fallback },
  12         },
  13     });
  14 
  15     pub const VerifyError = error{
  16         ScfIfMissingCondition,
  17         ScfIfMissingThenRegion,
  18         ScfIfMissingThenBlock,
  19         ScfIfMissingElseRegion,
  20         ScfIfMultipleBlocks,
  21         ScfIfYieldMissing,
  22         ScfIfYieldNotTerminal,
  23         ScfIfYieldArityMismatch,
  24         ScfIfYieldTypeMismatch,
  25         ScfForMissingBounds,
  26         ScfForRegionArityMismatch,
  27         ScfForMissingBody,
  28         ScfForMultipleBlocks,
  29         ScfForBlockArgCountMismatch,
  30         ScfForResultArityMismatch,
  31         ScfForBoundsTypeMismatch,
  32         ScfForInductionTypeMismatch,
  33         ScfForIterArgTypeMismatch,
  34         ScfForResultTypeMismatch,
  35         ScfForYieldMissing,
  36         ScfForYieldNotTerminal,
  37         ScfForYieldArityMismatch,
  38         ScfForYieldTypeMismatch,
  39         ScfForMultipleYields,
  40     };
  41 
  42     const yield_vtable = ir.interfaces.YieldOpInterface.VTable{
  43         .getYieldOperandCount = getYieldOperandCount,
  44         .getYieldOperand = getYieldOperand,
  45     };
  46 
  47     const ScfEval = struct {
  48         const arith = @import("arith/root.zig");
  49 
  50         fn canEval(op_ptr: *const anyopaque) bool {
  51             const op: *const ir.Operation = @ptrCast(@alignCast(op_ptr));
  52             if (std.mem.eql(u8, op.name.name, ForOp.operation_name)) {
  53                 return forInductionBits(op) != null;
  54             }
  55             return std.mem.eql(u8, op.name.name, IfOp.operation_name) or
  56                 std.mem.eql(u8, op.name.name, WhileOp.operation_name) or
  57                 std.mem.eql(u8, op.name.name, ConditionOp.operation_name) or
  58                 std.mem.eql(u8, op.name.name, YieldOp.operation_name);
  59         }
  60 
  61         /// The x64 guard and increment explicitly distinguish native 32/64-bit homes.
  62         fn forInductionBits(op: *const ir.Operation) ?u8 {
  63             if (op.operands.items.len < 3) return null;
  64             const typ = op.operands.items[0].value.type;
  65             if (!typ.eql(op.operands.items[1].value.type) or
  66                 !typ.eql(op.operands.items[2].value.type)) return null;
  67             return switch (arith.scalarKindFromType(typ) orelse return null) {
  68                 .index, .i64, .u64 => 64,
  69                 .i32, .u32 => 32,
  70                 else => null,
  71             };
  72         }
  73 
  74         fn arrayValues(attr: ir.Attribute) ?[]const ir.Attribute {
  75             const array_attr = attr.cast(ir.Attribute.ArrayAttr) orelse return null;
  76             return array_attr.getValues();
  77         }
  78 
  79         fn boolValue(attr: ir.Attribute) ?bool {
  80             const bool_attr = attr.cast(ir.Attribute.BoolAttr) orelse return null;
  81             return bool_attr.getValue();
  82         }
  83 
  84         fn makeArray(ctx: *ir.Context, values: []const ir.Attribute) ir.interfaces.EvalError!ir.Attribute {
  85             return ctx.getArrayAttr(values) catch error.OutOfMemory;
  86         }
  87 
  88         fn makeResults(ctx: *ir.Context, values: []const ir.Attribute, count: usize) ir.interfaces.EvalError!ir.Attribute {
  89             if (values.len != count) return error.InvalidOperand;
  90             if (count == 1) return values[0];
  91             return makeArray(ctx, values);
  92         }
  93 
  94         fn evaluateIf(
  95             op: *const ir.Operation,
  96             operands: []const ir.Attribute,
  97             eval_ctx: *const ir.interfaces.EvalContext,
  98         ) ir.interfaces.EvalError!ir.Attribute {
  99             if (operands.len != 1) return error.InvalidOperand;
 100             const if_op = IfOp{ .op = @constCast(op) };
 101             const take_then = boolValue(operands[0]) orelse return error.InvalidOperand;
 102             try eval_ctx.consumeBranchFuel(eval_ctx.state);
 103 
 104             const selected = if (take_then)
 105                 if_op.getThenRegion()
 106             else
 107                 if_op.getElseRegion() orelse {
 108                     if (op.results.items.len == 0) return makeArray(op.getContext(), &.{});
 109                     return error.InvalidOperand;
 110                 };
 111 
 112             const branch_attr = try eval_ctx.evaluateRegion(eval_ctx.state, selected);
 113             if (arrayValues(branch_attr)) |values| {
 114                 return makeResults(op.getContext(), values, op.results.items.len);
 115             }
 116             return makeResults(op.getContext(), &.{branch_attr}, op.results.items.len);
 117         }
 118 
 119         /// Each signed guard, including the exit guard, consumes both fuel budgets.
 120         /// Yield replaces the carried tuple before the wrapping induction increment.
 121         fn evaluateFor(
 122             op: *const ir.Operation,
 123             operands: []const ir.Attribute,
 124             eval_ctx: *const ir.interfaces.EvalContext,
 125         ) ir.interfaces.EvalError!ir.Attribute {
 126             const bits = forInductionBits(op) orelse return error.UnsupportedOperation;
 127             verifyForOpInternal(@constCast(op)) catch return error.InvalidOperand;
 128             std.debug.assert(operands.len == op.operands.items.len);
 129             const body = @constCast(op).getRegion(0) orelse return error.InvalidOperand;
 130             const lower = operands[0].cast(ir.Attribute.IntegerAttr) orelse return error.InvalidOperand;
 131             const upper = operands[1].cast(ir.Attribute.IntegerAttr) orelse return error.InvalidOperand;
 132             const step = operands[2].cast(ir.Attribute.IntegerAttr) orelse return error.InvalidOperand;
 133             var induction = arith.scalar.truncate(lower.getValue(), bits);
 134             const bound = arith.scalar.truncate(upper.getValue(), bits);
 135             const stride = step.getValue();
 136             const current = eval_ctx.allocator.alloc(ir.Attribute, operands.len - 2) catch return error.OutOfMemory;
 137             defer eval_ctx.allocator.free(current);
 138             const carried = current[1..];
 139             std.debug.assert(carried.len == op.results.items.len);
 140             @memcpy(carried, operands[3..]);
 141 
 142             while (true) {
 143                 try eval_ctx.consumeIterationFuel(eval_ctx.state);
 144                 try eval_ctx.consumeBranchFuel(eval_ctx.state);
 145                 if (induction >= bound) return makeResults(op.getContext(), carried, carried.len);
 146                 current[0] = op.getContext().getI64Attr(induction) catch return error.OutOfMemory;
 147                 const result = try eval_ctx.evaluateRegionWithArgs(eval_ctx.state, body, current);
 148                 const yielded = arrayValues(result) orelse return error.InvalidOperand;
 149                 if (yielded.len != carried.len) return error.InvalidOperand;
 150                 @memcpy(carried, yielded);
 151                 induction = arith.scalar.addWrap(induction, stride, bits);
 152             }
 153         }
 154 
 155         fn evaluateWhile(
 156             op: *const ir.Operation,
 157             operands: []const ir.Attribute,
 158             eval_ctx: *const ir.interfaces.EvalContext,
 159         ) ir.interfaces.EvalError!ir.Attribute {
 160             const while_op = WhileOp{ .op = @constCast(op) };
 161             const result_count = op.results.items.len;
 162             if (result_count != operands.len) return error.InvalidOperand;
 163 
 164             const current = eval_ctx.allocator.alloc(ir.Attribute, operands.len) catch return error.OutOfMemory;
 165             defer eval_ctx.allocator.free(current);
 166             @memcpy(current, operands);
 167 
 168             while (true) {
 169                 try eval_ctx.consumeIterationFuel(eval_ctx.state);
 170                 const condition_attr = try eval_ctx.evaluateRegionWithArgs(eval_ctx.state, while_op.getBeforeRegion(), current);
 171                 const condition_values = arrayValues(condition_attr) orelse return error.InvalidOperand;
 172                 if (condition_values.len != operands.len + 1) return error.InvalidOperand;
 173                 const keep_going = boolValue(condition_values[0]) orelse return error.InvalidOperand;
 174                 const carried = condition_values[1..];
 175                 if (!keep_going) return makeResults(op.getContext(), carried, result_count);
 176 
 177                 const yield_attr = try eval_ctx.evaluateRegionWithArgs(eval_ctx.state, while_op.getAfterRegion(), carried);
 178                 const yielded = arrayValues(yield_attr) orelse return error.InvalidOperand;
 179                 if (yielded.len != operands.len) return error.InvalidOperand;
 180                 @memcpy(current, yielded);
 181             }
 182         }
 183 
 184         fn evaluate(
 185             op_ptr: *const anyopaque,
 186             operands: []const ir.Attribute,
 187             eval_ctx: *const ir.interfaces.EvalContext,
 188         ) ir.interfaces.EvalError!ir.Attribute {
 189             const op: *const ir.Operation = @ptrCast(@alignCast(op_ptr));
 190             if (std.mem.eql(u8, op.name.name, IfOp.operation_name)) {
 191                 return evaluateIf(op, operands, eval_ctx);
 192             }
 193             if (std.mem.eql(u8, op.name.name, ForOp.operation_name)) {
 194                 return evaluateFor(op, operands, eval_ctx);
 195             }
 196             if (std.mem.eql(u8, op.name.name, WhileOp.operation_name)) {
 197                 return evaluateWhile(op, operands, eval_ctx);
 198             }
 199             if (std.mem.eql(u8, op.name.name, ConditionOp.operation_name)) {
 200                 if (operands.len == 0) return error.InvalidOperand;
 201                 return makeArray(op.getContext(), operands);
 202             }
 203             if (std.mem.eql(u8, op.name.name, YieldOp.operation_name)) {
 204                 return makeArray(op.getContext(), operands);
 205             }
 206             return error.UnsupportedOperation;
 207         }
 208 
 209         const vtable = ir.interfaces.Evaluatable.VTable{
 210             .canEval = canEval,
 211             .evaluate = evaluate,
 212         };
 213 
 214         fn fallback(_: *const ir.Operation) ?*const anyopaque {
 215             return &vtable;
 216         }
 217     };
 218 
 219     pub const IfOp = struct {
 220         op: *ir.Operation,
 221 
 222         const def = op_templates.explicit(@This(), .{
 223             .mnemonic = "if",
 224             .interfaces = &.{effects.EffectOpInterface.entryFor(.{
 225                 .capacity = .{ .entries = 1, .per_region = 1, .per_result = 1 },
 226                 .enumerate = conditionalEffects,
 227             })},
 228             .operands = .{"condition"},
 229             .regions = ir.dialects.shape.atLeast(1),
 230             .region_names = .{ "then", "else" },
 231             .successors = 0,
 232             .operand_types = &.{ir.dialects.typeConstraint.exact(0, "arith.bool")},
 233             .dynamic_traits = .{
 234                 ir.traits.AtLeastNRegions(1),
 235                 ir.traits.SingleBlock,
 236             },
 237         });
 238         pub const operation_spec = def.operation_spec;
 239         pub const operation_name = def.operation_name;
 240         pub const createOperation = def.createOperation;
 241         pub const getOperand = def.getOperand;
 242         pub const getRegion = def.getRegion;
 243         pub const verify = verifyIfOp;
 244         pub const verifyRegions = verifyIfOpRegions;
 245 
 246         pub fn create(
 247             ctx: *ir.Context,
 248             loc: ir.Location,
 249             condition: *ir.Value,
 250             result_types: []const ir.Type,
 251         ) !IfOp {
 252             try loadSpec(ctx);
 253             var then_body = ir.context.initRegion(ctx);
 254             defer then_body.deinit();
 255             var then_builder = ir.OperationBuilder.init(ctx);
 256             _ = try then_builder.createBlock(&then_body, &.{}, &.{});
 257             var else_body = ir.context.initRegion(ctx);
 258             defer else_body.deinit();
 259             var else_builder = ir.OperationBuilder.init(ctx);
 260             _ = try else_builder.createBlock(&else_body, &.{}, &.{});
 261             var regions = [_]*ir.Region{ &then_body, &else_body };
 262             return try @This().createOperation(ctx, loc, &.{condition}, result_types, &regions, &.{});
 263         }
 264 
 265         pub fn createWithoutElse(
 266             ctx: *ir.Context,
 267             loc: ir.Location,
 268             condition: *ir.Value,
 269         ) !IfOp {
 270             try loadSpec(ctx);
 271             var then_body = ir.context.initRegion(ctx);
 272             defer then_body.deinit();
 273             var then_builder = ir.OperationBuilder.init(ctx);
 274             _ = try then_builder.createBlock(&then_body, &.{}, &.{});
 275             var regions = [_]*ir.Region{&then_body};
 276             return try @This().createOperation(ctx, loc, &.{condition}, &.{}, &regions, &.{});
 277         }
 278 
 279         pub fn getCondition(self: IfOp) *ir.Value {
 280             return self.getOperand("condition");
 281         }
 282 
 283         pub fn getThenRegion(self: IfOp) *ir.Region {
 284             return self.getRegion("then");
 285         }
 286 
 287         pub fn getThenBlock(self: IfOp) *ir.Block {
 288             return self.getThenRegion().getEntryBlock().?;
 289         }
 290 
 291         pub fn getElseRegion(self: IfOp) ?*ir.Region {
 292             return self.op.getRegion(1);
 293         }
 294 
 295         pub fn getElseBlock(self: IfOp) ?*ir.Block {
 296             if (self.getElseRegion()) |region| {
 297                 return region.getEntryBlock();
 298             }
 299             return null;
 300         }
 301 
 302         pub fn getResult(self: *const IfOp, index: usize) ?*ir.Value {
 303             return self.op.getResult(index);
 304         }
 305 
 306         pub fn getNumResults(self: IfOp) usize {
 307             return self.op.results.items.len;
 308         }
 309     };
 310 
 311     pub const ForOp = struct {
 312         op: *ir.Operation,
 313 
 314         const def = op_templates.explicit(@This(), .{
 315             .mnemonic = "for",
 316             .interfaces = &.{effects.EffectOpInterface.entryFor(.{
 317                 .capacity = .{ .entries = 1, .per_region = 1, .per_result = 1 },
 318                 .enumerate = loopEffects,
 319             })},
 320             .operands = ir.dialects.shape.atLeast(3),
 321             .operand_names = .{ "lower_bound", "upper_bound", "step" },
 322             .regions = .{"body"},
 323             .successors = 0,
 324             .dynamic_traits = .{ ir.traits.OneRegion, ir.traits.SingleBlock },
 325         });
 326         pub const operation_spec = def.operation_spec;
 327         pub const operation_name = def.operation_name;
 328         pub const createOperation = def.createOperation;
 329         pub const getOperand = def.getOperand;
 330         pub const getRegion = def.getRegion;
 331         pub const verify = verifyForOp;
 332         pub const verifyRegions = verifyForOpRegions;
 333 
 334         pub fn create(
 335             ctx: *ir.Context,
 336             loc: ir.Location,
 337             lower_bound: *ir.Value,
 338             upper_bound: *ir.Value,
 339             step: *ir.Value,
 340             init_args: []const *ir.Value,
 341             result_types: []const ir.Type,
 342         ) !ForOp {
 343             try loadSpec(ctx);
 344             var operands: std.ArrayList(*ir.Value) = .empty;
 345             const allocator = ir.context.transientAllocator(ctx);
 346             defer operands.deinit(allocator);
 347             try operands.append(allocator, lower_bound);
 348             try operands.append(allocator, upper_bound);
 349             try operands.append(allocator, step);
 350             for (init_args) |arg| {
 351                 try operands.append(allocator, arg);
 352             }
 353             var body = ir.context.initRegion(ctx);
 354             defer body.deinit();
 355             var body_builder = ir.OperationBuilder.init(ctx);
 356             const body_block = try body_builder.createBlock(&body, &.{}, &.{});
 357 
 358             _ = try body_block.addArgument(lower_bound.type, loc);
 359 
 360             for (init_args) |init_arg| {
 361                 _ = try body_block.addArgument(init_arg.type, loc);
 362             }
 363             var regions = [_]*ir.Region{&body};
 364             return try @This().createOperation(ctx, loc, operands.items, result_types, &regions, &.{});
 365         }
 366 
 367         pub fn getLowerBound(self: ForOp) *ir.Value {
 368             return self.getOperand("lower_bound");
 369         }
 370 
 371         pub fn getUpperBound(self: ForOp) *ir.Value {
 372             return self.getOperand("upper_bound");
 373         }
 374 
 375         pub fn getStep(self: ForOp) *ir.Value {
 376             return self.getOperand("step");
 377         }
 378 
 379         pub fn getInitArgs(self: ForOp) []const *ir.Value {
 380             return self.op.getOperandValues()[3..];
 381         }
 382 
 383         pub fn getBodyRegion(self: ForOp) *ir.Region {
 384             return self.getRegion("body");
 385         }
 386 
 387         pub fn getBodyBlock(self: ForOp) *ir.Block {
 388             return self.getBodyRegion().getEntryBlock().?;
 389         }
 390 
 391         pub fn getInductionVar(self: ForOp) *ir.Value {
 392             return self.getBodyBlock().arguments.items[0];
 393         }
 394 
 395         pub fn getIterArgs(self: ForOp) []*ir.Value {
 396             return self.getBodyBlock().arguments.items[1..];
 397         }
 398 
 399         pub fn getResult(self: *const ForOp, index: usize) ?*ir.Value {
 400             return self.op.getResult(index);
 401         }
 402     };
 403 
 404     pub const WhileOp = struct {
 405         op: *ir.Operation,
 406 
 407         const def = op_templates.explicit(@This(), .{
 408             .mnemonic = "while",
 409             .interfaces = &.{effects.EffectOpInterface.entryFor(.{
 410                 .capacity = .{ .entries = 1, .per_region = 1, .per_result = 1 },
 411                 .enumerate = loopEffects,
 412             })},
 413             .regions = 2,
 414             .region_names = .{ "before", "after" },
 415             .successors = 0,
 416             .dynamic_traits = .{ir.traits.SingleBlock},
 417         });
 418         pub const operation_spec = def.operation_spec;
 419         pub const operation_name = def.operation_name;
 420         pub const createOperation = def.createOperation;
 421         pub const getRegion = def.getRegion;
 422 
 423         pub fn create(
 424             ctx: *ir.Context,
 425             loc: ir.Location,
 426             init_args: []const *ir.Value,
 427             result_types: []const ir.Type,
 428         ) !WhileOp {
 429             try loadSpec(ctx);
 430             var operands: std.ArrayList(*ir.Value) = .empty;
 431             const allocator = ir.context.transientAllocator(ctx);
 432             defer operands.deinit(allocator);
 433             for (init_args) |arg| {
 434                 try operands.append(allocator, arg);
 435             }
 436 
 437             var before_body = ir.context.initRegion(ctx);
 438             defer before_body.deinit();
 439             var before_builder = ir.OperationBuilder.init(ctx);
 440             const before_block = try before_builder.createBlock(&before_body, &.{}, &.{});
 441             for (init_args) |init_arg| {
 442                 _ = try before_block.addArgument(init_arg.type, loc);
 443             }
 444 
 445             var after_body = ir.context.initRegion(ctx);
 446             defer after_body.deinit();
 447             var after_builder = ir.OperationBuilder.init(ctx);
 448             const after_block = try after_builder.createBlock(&after_body, &.{}, &.{});
 449             for (init_args) |init_arg| {
 450                 _ = try after_block.addArgument(init_arg.type, loc);
 451             }
 452             var regions = [_]*ir.Region{ &before_body, &after_body };
 453             return try @This().createOperation(ctx, loc, operands.items, result_types, &regions, &.{});
 454         }
 455 
 456         pub fn getBeforeRegion(self: WhileOp) *ir.Region {
 457             return self.getRegion("before");
 458         }
 459 
 460         pub fn getAfterRegion(self: WhileOp) *ir.Region {
 461             return self.getRegion("after");
 462         }
 463 
 464         pub fn getBeforeBlock(self: WhileOp) *ir.Block {
 465             return self.getBeforeRegion().getEntryBlock().?;
 466         }
 467 
 468         pub fn getAfterBlock(self: WhileOp) *ir.Block {
 469             return self.getAfterRegion().getEntryBlock().?;
 470         }
 471     };
 472 
 473     pub const YieldOp = struct {
 474         op: *ir.Operation,
 475 
 476         const term = op_templates.explicitTerminator(@This(), .{
 477             .mnemonic = "yield",
 478             .interfaces = &.{
 479                 ir.interfaces.YieldOpInterface.entry(&yield_vtable),
 480                 effects.EffectOpInterface.entryFor(.{}),
 481             },
 482         });
 483         pub const operation_spec = term.operation_spec;
 484         pub const operation_name = term.operation_name;
 485         pub const createTerminator = term.createTerminator;
 486 
 487         pub fn create(
 488             ctx: *ir.Context,
 489             loc: ir.Location,
 490             results: []const *ir.Value,
 491         ) !YieldOp {
 492             try loadSpec(ctx);
 493             return try @This().createTerminator(ctx, loc, results, &.{});
 494         }
 495 
 496         pub fn getOperands(self: YieldOp) []const *ir.Value {
 497             return self.op.getOperandValues();
 498         }
 499     };
 500 
 501     pub const ConditionOp = struct {
 502         op: *ir.Operation,
 503 
 504         const term = op_templates.explicitTerminator(@This(), .{
 505             .mnemonic = "condition",
 506             .interfaces = &.{effects.EffectOpInterface.entryFor(.{})},
 507             .operands = ir.dialects.shape.atLeast(1),
 508             .operand_names = .{"condition"},
 509             .operand_types = &.{ir.dialects.typeConstraint.exact(0, "arith.bool")},
 510         });
 511         pub const operation_spec = term.operation_spec;
 512         pub const operation_name = term.operation_name;
 513         pub const createTerminator = term.createTerminator;
 514         pub const getOperand = term.getOperand;
 515 
 516         pub fn create(
 517             ctx: *ir.Context,
 518             loc: ir.Location,
 519             condition: *ir.Value,
 520             args: []const *ir.Value,
 521         ) !ConditionOp {
 522             try loadSpec(ctx);
 523             var operands: std.ArrayList(*ir.Value) = .empty;
 524             const allocator = ir.context.transientAllocator(ctx);
 525             defer operands.deinit(allocator);
 526             try operands.append(allocator, condition);
 527             for (args) |arg| {
 528                 try operands.append(allocator, arg);
 529             }
 530             return try @This().createTerminator(ctx, loc, operands.items, &.{});
 531         }
 532 
 533         pub fn getCondition(self: ConditionOp) *ir.Value {
 534             return self.getOperand("condition");
 535         }
 536 
 537         pub fn getArgs(self: ConditionOp) []const *ir.Value {
 538             return self.op.getOperandValues()[1..];
 539         }
 540     };
 541 
 542     fn loadSpec(ctx: *ir.Context) !void {
 543         try ir.dialects.loadDialectSpec(ctx, spec);
 544     }
 545 
 546     fn getYieldOperandCount(op_ptr: *const anyopaque) usize {
 547         const op: *const ir.Operation = @ptrCast(@alignCast(op_ptr));
 548         return op.getOperandValues().len;
 549     }
 550 
 551     fn getYieldOperand(op_ptr: *const anyopaque, index: usize) ?*ir.Value {
 552         const op: *const ir.Operation = @ptrCast(@alignCast(op_ptr));
 553         return op.getOperand(index);
 554     }
 555 
 556     fn verifyForOp(op_ptr: *const anyopaque) anyerror!void {
 557         const op: *ir.Operation = @ptrCast(@alignCast(@constCast(op_ptr)));
 558         if (op.operands.items.len < 3) return error.ScfForMissingBounds;
 559         if (op.regions.items.len != 1) return error.ScfForRegionArityMismatch;
 560     }
 561 
 562     fn verifyIfOp(op_ptr: *const anyopaque) anyerror!void {
 563         const op: *ir.Operation = @ptrCast(@alignCast(@constCast(op_ptr)));
 564         if (op.operands.items.len != 1) return error.ScfIfMissingCondition;
 565         if (op.regions.items.len < 1) return error.ScfIfMissingThenRegion;
 566         if (op.results.items.len > 0 and op.regions.items.len < 2) return error.ScfIfMissingElseRegion;
 567     }
 568 
 569     fn verifyForOpRegions(op_ptr: *const anyopaque) anyerror!void {
 570         const op: *ir.Operation = @ptrCast(@alignCast(@constCast(op_ptr)));
 571         try verifyForOpInternal(op);
 572     }
 573 
 574     fn verifyIfOpRegions(op_ptr: *const anyopaque) anyerror!void {
 575         const op: *ir.Operation = @ptrCast(@alignCast(@constCast(op_ptr)));
 576         try verifyIfOpInternal(op);
 577     }
 578 
 579     fn verifyIfOpInternal(op: *ir.Operation) VerifyError!void {
 580         if (op.operands.items.len != 1) return error.ScfIfMissingCondition;
 581         if (op.regions.items.len < 1) return error.ScfIfMissingThenRegion;
 582 
 583         const result_count = op.results.items.len;
 584         if (result_count == 0) return;
 585         if (op.regions.items.len < 2) return error.ScfIfMissingElseRegion;
 586 
 587         try verifyIfYield(op, op.getRegion(0) orelse return error.ScfIfMissingThenRegion);
 588         try verifyIfYield(op, op.getRegion(1) orelse return error.ScfIfMissingElseRegion);
 589     }
 590 
 591     fn verifyIfYield(op: *ir.Operation, region: *ir.Region) VerifyError!void {
 592         const block = region.getEntryBlock() orelse return error.ScfIfMissingThenBlock;
 593         if (!region.hasOneBlock()) return error.ScfIfMultipleBlocks;
 594 
 595         var yield_op: ?*ir.Operation = null;
 596         var op_iter = block.operations.head;
 597         while (op_iter) |opaque_op| {
 598             const current: *ir.Operation = @ptrCast(@alignCast(opaque_op));
 599             if (std.mem.eql(u8, current.name.name, YieldOp.operation_name)) {
 600                 if (current.next_op != null) return error.ScfIfYieldNotTerminal;
 601                 yield_op = current;
 602             }
 603             op_iter = current.next_op;
 604         }
 605 
 606         const yield_final = yield_op orelse return error.ScfIfYieldMissing;
 607         if (yield_final.operands.items.len != op.results.items.len) return error.ScfIfYieldArityMismatch;
 608 
 609         for (op.results.items, 0..) |result, index| {
 610             const yield_type = yield_final.operands.items[index].value.type;
 611             if (!result.type.eql(yield_type)) return error.ScfIfYieldTypeMismatch;
 612         }
 613     }
 614 
 615     fn verifyForOpInternal(op: *ir.Operation) VerifyError!void {
 616         if (op.operands.items.len < 3) return error.ScfForMissingBounds;
 617         if (op.regions.items.len != 1) return error.ScfForRegionArityMismatch;
 618 
 619         const body_region = op.getRegion(0) orelse return error.ScfForMissingBody;
 620         const body_block = body_region.getEntryBlock() orelse return error.ScfForMissingBody;
 621         if (!body_region.hasOneBlock()) return error.ScfForMultipleBlocks;
 622 
 623         const iter_count = op.operands.items.len - 3;
 624         if (op.results.items.len != iter_count) return error.ScfForResultArityMismatch;
 625         if (body_block.arguments.items.len != iter_count + 1) return error.ScfForBlockArgCountMismatch;
 626 
 627         const lower_type = op.operands.items[0].value.type;
 628         const upper_type = op.operands.items[1].value.type;
 629         const step_type = op.operands.items[2].value.type;
 630         if (!lower_type.eql(upper_type) or !lower_type.eql(step_type)) {
 631             return error.ScfForBoundsTypeMismatch;
 632         }
 633         if (!body_block.arguments.items[0].type.eql(lower_type)) {
 634             return error.ScfForInductionTypeMismatch;
 635         }
 636 
 637         var yield_op: ?*ir.Operation = null;
 638         var op_iter = body_block.operations.head;
 639         while (op_iter) |opaque_op| {
 640             const current: *ir.Operation = @ptrCast(@alignCast(opaque_op));
 641             if (std.mem.eql(u8, current.name.name, YieldOp.operation_name)) {
 642                 if (yield_op != null) return error.ScfForMultipleYields;
 643                 if (current.next_op != null) return error.ScfForYieldNotTerminal;
 644                 yield_op = current;
 645             }
 646             op_iter = current.next_op;
 647         }
 648 
 649         const yield_final = yield_op orelse return error.ScfForYieldMissing;
 650         if (yield_final.operands.items.len != iter_count) return error.ScfForYieldArityMismatch;
 651 
 652         for (0..iter_count) |i| {
 653             const init_type = op.operands.items[3 + i].value.type;
 654             const block_type = body_block.arguments.items[1 + i].type;
 655             if (!init_type.eql(block_type)) return error.ScfForIterArgTypeMismatch;
 656             const result_type = op.results.items[i].type;
 657             if (!init_type.eql(result_type)) return error.ScfForResultTypeMismatch;
 658             const yield_type = yield_final.operands.items[i].value.type;
 659             if (!init_type.eql(yield_type)) return error.ScfForYieldTypeMismatch;
 660         }
 661     }
 662 };
 663 
 664 test "ScfDialect.IfOp creates conditional" {
 665     const testing = std.testing;
 666     const arith = @import("arith/root.zig");
 667 
 668     var arena = alloc_arena.Arena.init(std.testing.allocator);
 669     defer arena.deinit();
 670     const allocator = arena.allocator();
 671 
 672     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
 673     defer ctx.deinit(allocator);
 674 
 675     const loc = ir.Location.getUnknown();
 676     const i32_type = try arith.ArithDialect.getI32Type(&ctx);
 677 
 678     var cond = try arith.ArithDialect.ConstantOp.createBool(&ctx, loc, true);
 679 
 680     const if_op = try ScfDialect.IfOp.create(&ctx, loc, cond.getResult(), &.{i32_type});
 681 
 682     try testing.expectEqualStrings("scf.if", if_op.op.name.name);
 683     try testing.expect(if_op.getCondition() == cond.getResult());
 684     _ = if_op.getThenBlock();
 685     try testing.expect(if_op.getElseBlock() != null);
 686     try testing.expectEqual(@as(usize, 1), if_op.getNumResults());
 687 }
 688 
 689 test "ScfDialect spec owns verifier and terminator traits" {
 690     const testing = std.testing;
 691 
 692     var arena = alloc_arena.Arena.init(std.testing.allocator);
 693     defer arena.deinit();
 694     const allocator = arena.allocator();
 695 
 696     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
 697     defer ctx.deinit(allocator);
 698 
 699     try ir.dialects.loadDialectSpec(&ctx, ScfDialect.spec);
 700 
 701     const if_info = ctx.lookupOperation(ScfDialect.IfOp.operation_name) orelse
 702         return error.TestExpectedOperationInfo;
 703     try testing.expect(if_info.hasInterface(ir.VerifyOpInterface.id));
 704     try testing.expect(if_info.hasInterface(ir.VerifyRegionOpInterface.id));
 705     try testing.expectEqual(@as(usize, 1), if_info.getOperandTypeConstraints().len);
 706     try testing.expectEqual(@as(usize, 0), if_info.getOperandTypeConstraints()[0].index);
 707     try testing.expectEqualStrings("arith.bool", if_info.getOperandTypeConstraints()[0].type_name);
 708     try testing.expect(if_info.hasTraitId(ir.traits.AtLeastNRegions(1).id));
 709     try testing.expect(if_info.hasTraitId(ir.traits.SingleBlock.id));
 710 
 711     const for_info = ctx.lookupOperation(ScfDialect.ForOp.operation_name) orelse
 712         return error.TestExpectedOperationInfo;
 713     try testing.expect(for_info.hasInterface(ir.VerifyOpInterface.id));
 714     try testing.expect(for_info.hasInterface(ir.VerifyRegionOpInterface.id));
 715     try testing.expect(for_info.hasTraitId(ir.traits.OneRegion.id));
 716     try testing.expect(for_info.hasTraitId(ir.traits.SingleBlock.id));
 717 
 718     const while_info = ctx.lookupOperation(ScfDialect.WhileOp.operation_name) orelse
 719         return error.TestExpectedOperationInfo;
 720     try testing.expect(while_info.hasTraitId(ir.traits.SingleBlock.id));
 721 
 722     const yield_info = ctx.lookupOperation(ScfDialect.YieldOp.operation_name) orelse
 723         return error.TestExpectedOperationInfo;
 724     try testing.expect(yield_info.traits.is_terminator);
 725     try testing.expect(yield_info.hasTraitId(ir.traits.Terminator.id));
 726     try testing.expect(yield_info.hasInterface(ir.interfaces.YieldOpInterface.id));
 727 
 728     const condition_info = ctx.lookupOperation(ScfDialect.ConditionOp.operation_name) orelse
 729         return error.TestExpectedOperationInfo;
 730     try testing.expect(condition_info.traits.is_terminator);
 731     try testing.expect(condition_info.hasTraitId(ir.traits.Terminator.id));
 732     try testing.expectEqual(@as(usize, 1), condition_info.getOperandTypeConstraints().len);
 733     try testing.expectEqual(@as(usize, 0), condition_info.getOperandTypeConstraints()[0].index);
 734     try testing.expectEqualStrings("arith.bool", condition_info.getOperandTypeConstraints()[0].type_name);
 735 }
 736 
 737 test "ScfDialect.IfOp verifier accepts result yields" {
 738     const arith = @import("arith/root.zig");
 739 
 740     var arena = alloc_arena.Arena.init(std.testing.allocator);
 741     defer arena.deinit();
 742     const allocator = arena.allocator();
 743 
 744     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
 745     defer ctx.deinit(allocator);
 746 
 747     const loc = ir.Location.getUnknown();
 748     const i32_type = try arith.ArithDialect.getI32Type(&ctx);
 749 
 750     var cond = try arith.ArithDialect.ConstantOp.createBool(&ctx, loc, true);
 751     const if_op = try ScfDialect.IfOp.create(&ctx, loc, cond.getResult(), &.{i32_type});
 752 
 753     var then_value = try arith.ArithDialect.ConstantOp.createInt(&ctx, loc, i32_type, 1);
 754     try if_op.getThenBlock().addOperation(then_value.op);
 755     const then_yield = try ScfDialect.YieldOp.create(&ctx, loc, &.{then_value.getResult()});
 756     try if_op.getThenBlock().addOperation(then_yield.op);
 757 
 758     var else_value = try arith.ArithDialect.ConstantOp.createInt(&ctx, loc, i32_type, 2);
 759     try if_op.getElseBlock().?.addOperation(else_value.op);
 760     const else_yield = try ScfDialect.YieldOp.create(&ctx, loc, &.{else_value.getResult()});
 761     try if_op.getElseBlock().?.addOperation(else_yield.op);
 762 
 763     try ir.verifyOperation(if_op.op, ir.verify.default_options);
 764 }
 765 
 766 test "ScfDialect.IfOp verifier rejects missing result yield" {
 767     const testing = std.testing;
 768     const arith = @import("arith/root.zig");
 769 
 770     var arena = alloc_arena.Arena.init(std.testing.allocator);
 771     defer arena.deinit();
 772     const allocator = arena.allocator();
 773 
 774     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
 775     defer ctx.deinit(allocator);
 776 
 777     const loc = ir.Location.getUnknown();
 778     const i32_type = try arith.ArithDialect.getI32Type(&ctx);
 779 
 780     var cond = try arith.ArithDialect.ConstantOp.createBool(&ctx, loc, true);
 781     const if_op = try ScfDialect.IfOp.create(&ctx, loc, cond.getResult(), &.{i32_type});
 782 
 783     const result = ir.verifyOperation(if_op.op, ir.verify.default_options);
 784     try testing.expectError(ScfDialect.VerifyError.ScfIfYieldMissing, result);
 785 }
 786 
 787 test "ScfDialect.ForOp creates for loop" {
 788     const testing = std.testing;
 789     const arith = @import("arith/root.zig");
 790 
 791     var arena = alloc_arena.Arena.init(std.testing.allocator);
 792     defer arena.deinit();
 793     const allocator = arena.allocator();
 794 
 795     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
 796     defer ctx.deinit(allocator);
 797 
 798     const loc = ir.Location.getUnknown();
 799     const index_type = try arith.ArithDialect.getIndexType(&ctx);
 800     const f32_type = try arith.ArithDialect.getScalarType(&ctx, .f32);
 801 
 802     var lo = try arith.ArithDialect.ConstantOp.createInt(&ctx, loc, index_type, 0);
 803     var hi = try arith.ArithDialect.ConstantOp.createInt(&ctx, loc, index_type, 100);
 804     var step = try arith.ArithDialect.ConstantOp.createInt(&ctx, loc, index_type, 1);
 805 
 806     var init = try arith.ArithDialect.ConstantOp.createFloat(&ctx, loc, f32_type, 0.0);
 807 
 808     var for_op = try ScfDialect.ForOp.create(
 809         &ctx,
 810         loc,
 811         lo.getResult(),
 812         hi.getResult(),
 813         step.getResult(),
 814         &.{init.getResult()},
 815         &.{f32_type},
 816     );
 817 
 818     try testing.expectEqualStrings("scf.for", for_op.op.name.name);
 819     try testing.expect(for_op.getLowerBound() == lo.getResult());
 820     try testing.expect(for_op.getUpperBound() == hi.getResult());
 821     try testing.expect(for_op.getStep() == step.getResult());
 822     const init_args = for_op.getInitArgs();
 823     try testing.expectEqual(@as(usize, 1), init_args.len);
 824     try testing.expect(init_args[0] == init.getResult());
 825 
 826     try testing.expectEqual(@as(usize, 2), for_op.getBodyBlock().arguments.items.len);
 827 }
 828 
 829 test "ScfDialect.YieldOp and ConditionOp expose const operand slices" {
 830     const testing = std.testing;
 831     const arith = @import("arith/root.zig");
 832 
 833     var arena = alloc_arena.Arena.init(std.testing.allocator);
 834     defer arena.deinit();
 835     const allocator = arena.allocator();
 836 
 837     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
 838     defer ctx.deinit(allocator);
 839 
 840     const loc = ir.Location.getUnknown();
 841     const bool_type = try arith.ArithDialect.getScalarType(&ctx, .bool);
 842 
 843     var cond = try arith.ArithDialect.ConstantOp.createBool(&ctx, loc, true);
 844     var arg = try arith.ArithDialect.ConstantOp.createBool(&ctx, loc, false);
 845 
 846     const yield_op = try ScfDialect.YieldOp.create(&ctx, loc, &.{ cond.getResult(), arg.getResult() });
 847     const yield_operands = yield_op.getOperands();
 848     try testing.expectEqual(@as(usize, 2), yield_operands.len);
 849     try testing.expect(yield_operands[0] == cond.getResult());
 850     try testing.expect(yield_operands[1] == arg.getResult());
 851 
 852     const iface = yield_op.op.interface(ir.interfaces.YieldOpInterface).?;
 853     try testing.expectEqual(@as(usize, 2), iface.call(.getYieldOperandCount, .{}));
 854     try testing.expect(iface.call(.getYieldOperand, .{0}).? == cond.getResult());
 855     try testing.expect(iface.call(.getYieldOperand, .{1}).? == arg.getResult());
 856     try testing.expectEqual(@as(?*ir.Value, null), iface.call(.getYieldOperand, .{2}));
 857 
 858     const condition_op = try ScfDialect.ConditionOp.create(&ctx, loc, cond.getResult(), &.{arg.getResult()});
 859     try testing.expect(condition_op.getCondition() == cond.getResult());
 860     const condition_args = condition_op.getArgs();
 861     try testing.expectEqual(@as(usize, 1), condition_args.len);
 862     try testing.expect(condition_args[0] == arg.getResult());
 863 
 864     _ = bool_type;
 865 }
 866 
 867 test "ScfDialect.ForOp verifier accepts well-formed loops" {
 868     const arith = @import("arith/root.zig");
 869 
 870     var arena = alloc_arena.Arena.init(std.testing.allocator);
 871     defer arena.deinit();
 872     const allocator = arena.allocator();
 873 
 874     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
 875     defer ctx.deinit(allocator);
 876 
 877     const loc = ir.Location.getUnknown();
 878     const index_type = try arith.ArithDialect.getIndexType(&ctx);
 879 
 880     var lo = try arith.ArithDialect.ConstantOp.createInt(&ctx, loc, index_type, 0);
 881     var hi = try arith.ArithDialect.ConstantOp.createInt(&ctx, loc, index_type, 4);
 882     var step = try arith.ArithDialect.ConstantOp.createInt(&ctx, loc, index_type, 1);
 883     var init = try arith.ArithDialect.ConstantOp.createInt(&ctx, loc, index_type, 0);
 884 
 885     var for_op = try ScfDialect.ForOp.create(
 886         &ctx,
 887         loc,
 888         lo.getResult(),
 889         hi.getResult(),
 890         step.getResult(),
 891         &.{init.getResult()},
 892         &.{index_type},
 893     );
 894 
 895     const body_block = for_op.getBodyBlock();
 896     const acc = body_block.arguments.items[1];
 897     const yield_op = try ScfDialect.YieldOp.create(&ctx, loc, &.{acc});
 898     try body_block.addOperation(yield_op.op);
 899 
 900     try ir.verifyOperation(for_op.op, ir.verify.default_options);
 901 }
 902 
 903 test "ScfDialect.ForOp verifier accepts zero iter_args" {
 904     const arith = @import("arith/root.zig");
 905 
 906     var arena = alloc_arena.Arena.init(std.testing.allocator);
 907     defer arena.deinit();
 908     const allocator = arena.allocator();
 909 
 910     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
 911     defer ctx.deinit(allocator);
 912 
 913     const loc = ir.Location.getUnknown();
 914     const index_type = try arith.ArithDialect.getIndexType(&ctx);
 915 
 916     var lo = try arith.ArithDialect.ConstantOp.createInt(&ctx, loc, index_type, 0);
 917     var hi = try arith.ArithDialect.ConstantOp.createInt(&ctx, loc, index_type, 4);
 918     var step = try arith.ArithDialect.ConstantOp.createInt(&ctx, loc, index_type, 1);
 919 
 920     var for_op = try ScfDialect.ForOp.create(
 921         &ctx,
 922         loc,
 923         lo.getResult(),
 924         hi.getResult(),
 925         step.getResult(),
 926         &.{},
 927         &.{},
 928     );
 929 
 930     const body_block = for_op.getBodyBlock();
 931     const yield_op = try ScfDialect.YieldOp.create(&ctx, loc, &.{});
 932     try body_block.addOperation(yield_op.op);
 933 
 934     try ir.verifyOperation(for_op.op, ir.verify.default_options);
 935 }
 936 
 937 test "ScfDialect.ForOp verifier rejects mismatched yield arity" {
 938     const testing = std.testing;
 939     const arith = @import("arith/root.zig");
 940 
 941     var arena = alloc_arena.Arena.init(std.testing.allocator);
 942     defer arena.deinit();
 943     const allocator = arena.allocator();
 944 
 945     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
 946     defer ctx.deinit(allocator);
 947 
 948     const loc = ir.Location.getUnknown();
 949     const index_type = try arith.ArithDialect.getIndexType(&ctx);
 950 
 951     var lo = try arith.ArithDialect.ConstantOp.createInt(&ctx, loc, index_type, 0);
 952     var hi = try arith.ArithDialect.ConstantOp.createInt(&ctx, loc, index_type, 2);
 953     var step = try arith.ArithDialect.ConstantOp.createInt(&ctx, loc, index_type, 1);
 954     var init = try arith.ArithDialect.ConstantOp.createInt(&ctx, loc, index_type, 0);
 955 
 956     var for_op = try ScfDialect.ForOp.create(
 957         &ctx,
 958         loc,
 959         lo.getResult(),
 960         hi.getResult(),
 961         step.getResult(),
 962         &.{init.getResult()},
 963         &.{index_type},
 964     );
 965 
 966     const body_block = for_op.getBodyBlock();
 967     const yield_op = try ScfDialect.YieldOp.create(&ctx, loc, &.{});
 968     try body_block.addOperation(yield_op.op);
 969 
 970     const result = ir.verifyOperation(for_op.op, ir.verify.default_options);
 971     try testing.expectError(ScfDialect.VerifyError.ScfForYieldArityMismatch, result);
 972 }
 973 
 974 test "scf.if with non-bool cond operand fails verification" {
 975     const testing = std.testing;
 976     var arena = alloc_arena.Arena.init(std.testing.allocator);
 977     defer arena.deinit();
 978     const allocator = arena.allocator();
 979 
 980     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
 981     defer ctx.deinit(allocator);
 982     try ir.dialects.loadDialectSpec(&ctx, ScfDialect.spec);
 983 
 984     const arith = @import("arith/root.zig");
 985     const loc = ir.Location.getUnknown();
 986     const i32_type = try arith.ArithDialect.getI32Type(&ctx);
 987 
 988     var cond = try arith.ArithDialect.ConstantOp.createInt(&ctx, loc, i32_type, 1);
 989     const if_op = try ScfDialect.IfOp.create(&ctx, loc, cond.getResult(), &.{});
 990 
 991     try testing.expectError(
 992         ir.VerifyError.OperandTypeConstraintMismatch,
 993         ir.verifyOperation(if_op.op, .{ .recursive = false }),
 994     );
 995 }
 996 
 997 test "scf.if with bool cond operand passes verification" {
 998     var arena = alloc_arena.Arena.init(std.testing.allocator);
 999     defer arena.deinit();
1000     const allocator = arena.allocator();
1001 
1002     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1003     defer ctx.deinit(allocator);
1004     try ir.dialects.loadDialectSpec(&ctx, ScfDialect.spec);
1005 
1006     const arith = @import("arith/root.zig");
1007     const loc = ir.Location.getUnknown();
1008 
1009     var cond = try arith.ArithDialect.ConstantOp.createBool(&ctx, loc, true);
1010     const if_op = try ScfDialect.IfOp.create(&ctx, loc, cond.getResult(), &.{});
1011 
1012     try ir.verifyOperation(if_op.op, .{ .recursive = false });
1013 }
1014 
1015 test "scf.condition with non-bool cond operand fails verification" {
1016     const testing = std.testing;
1017     var arena = alloc_arena.Arena.init(std.testing.allocator);
1018     defer arena.deinit();
1019     const allocator = arena.allocator();
1020 
1021     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1022     defer ctx.deinit(allocator);
1023     try ir.dialects.loadDialectSpec(&ctx, ScfDialect.spec);
1024 
1025     const arith = @import("arith/root.zig");
1026     const loc = ir.Location.getUnknown();
1027     const i32_type = try arith.ArithDialect.getI32Type(&ctx);
1028 
1029     var cond = try arith.ArithDialect.ConstantOp.createInt(&ctx, loc, i32_type, 1);
1030     const condition_op = try ScfDialect.ConditionOp.create(&ctx, loc, cond.getResult(), &.{});
1031 
1032     try testing.expectError(
1033         ir.VerifyError.OperandTypeConstraintMismatch,
1034         ir.verifyOperation(condition_op.op, .{ .recursive = false }),
1035     );
1036 }
1037 
1038 test "scf.condition with bool cond operand passes verification" {
1039     var arena = alloc_arena.Arena.init(std.testing.allocator);
1040     defer arena.deinit();
1041     const allocator = arena.allocator();
1042 
1043     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1044     defer ctx.deinit(allocator);
1045     try ir.dialects.loadDialectSpec(&ctx, ScfDialect.spec);
1046 
1047     const arith = @import("arith/root.zig");
1048     const loc = ir.Location.getUnknown();
1049 
1050     var cond = try arith.ArithDialect.ConstantOp.createBool(&ctx, loc, true);
1051     const condition_op = try ScfDialect.ConditionOp.create(&ctx, loc, cond.getResult(), &.{});
1052 
1053     try ir.verifyOperation(condition_op.op, .{ .recursive = false });
1054 }
1055 
1056 fn conditionalEffects(op: *const ir.Operation, collector: *effects.Collector) void {
1057     regionEffects(op, collector, .conditional);
1058 }
1059 
1060 fn loopEffects(op: *const ir.Operation, collector: *effects.Collector) void {
1061     collector.append(.{ .event = .{ .kind = .diverge } });
1062     regionEffects(op, collector, .repeated);
1063 }
1064 
1065 fn regionEffects(
1066     op: *const ir.Operation,
1067     collector: *effects.Collector,
1068     execution: effects.Execution,
1069 ) void {
1070     for (0..op.getNumRegions()) |index| collector.append(.{ .region = .{
1071         .index = index,
1072         .execution = execution,
1073         .may_diverge = execution == .repeated,
1074     } });
1075     for (0..op.getNumResults()) |index| collector.append(.{ .result = .{ .index = index } });
1076 }
1077 
1078 test "scf effect declarations distinguish conditional and repeated execution" {
1079     const arithmetic = @import("arith/root.zig").ArithDialect;
1080     var ctx = try ir.Context.init(std.testing.allocator, ir.Context.Limits.testing);
1081     defer ctx.deinit(std.testing.allocator);
1082     const typ = try arithmetic.getScalarType(&ctx, .index);
1083     const zero = try arithmetic.ConstantOp.createInt(&ctx, .unknown, typ, 0);
1084     const one = try arithmetic.ConstantOp.createInt(&ctx, .unknown, typ, 1);
1085     const condition = try arithmetic.ConstantOp.createBool(&ctx, .unknown, true);
1086     const branch = try ScfDialect.IfOp.create(&ctx, .unknown, condition.getResult(), &.{});
1087     const loop = try ScfDialect.ForOp.create(
1088         &ctx,
1089         .unknown,
1090         zero.getResult(),
1091         zero.getResult(),
1092         one.getResult(),
1093         &.{},
1094         &.{},
1095     );
1096     var branch_facts = try effects.inspect(std.testing.allocator, branch.op);
1097     defer branch_facts.deinit(std.testing.allocator);
1098     try std.testing.expectEqual(@as(usize, 2), branch_facts.facts.records.len);
1099     for (branch_facts.facts.records) |fact| {
1100         try std.testing.expectEqual(effects.Execution.conditional, fact.region.execution);
1101     }
1102     var loop_facts = try effects.inspect(std.testing.allocator, loop.op);
1103     defer loop_facts.deinit(std.testing.allocator);
1104     try std.testing.expectEqual(effects.EventKind.diverge, loop_facts.facts.records[0].event.kind);
1105     try std.testing.expectEqual(
1106         effects.Execution.repeated,
1107         loop_facts.facts.records[1].region.execution,
1108     );
1109     try std.testing.expect(!effects.speculate(loop_facts.facts, true));
1110 }