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, ®ions, &.{});
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}, &.{}, ®ions, &.{});
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, ®ions, &.{});
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, ®ions, &.{});
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 }