lib/choir/src/eval/evaluator.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const alloc_arena = @import("alloc_arena");
  3 const alloc_observe = @import("alloc_observe");
  4 const ir = @import("../core/root.zig");
  5 const interfaces = @import("../core/root.zig").interfaces;
  6 const rewrite_builder = @import("rewrite.zig");
  7 
  8 pub const Evaluator = struct {
  9     allocator: std.mem.Allocator,
 10     ir_ctx: *ir.Context,
 11 
 12     branch_fuel: u32 = 10_000,
 13     iteration_fuel: u32 = 100_000,
 14     recursion_depth: u32 = 0,
 15     max_recursion: u32 = 256,
 16     allocation_used: u64 = 0,
 17     allocation_cap: u64 = 64 * 1024 * 1024,
 18 
 19     comptime_gpa: alloc_observe.debug.Allocator(.{}),
 20 
 21     current_loc: ir.Location,
 22     diagnostics: std.ArrayListUnmanaged(Diagnostic),
 23 
 24     frames: std.ArrayListUnmanaged(ValueFrame),
 25 
 26     root_op: ?*ir.Operation = null,
 27     symbol_table: ir.SymbolTable,
 28 
 29     handle_table: std.ArrayList(HandleEntry),
 30     rewrite_builders: std.ArrayListUnmanaged(*rewrite_builder.RewriteBuilder),
 31 
 32     pub const Diagnostic = struct {
 33         kind: interfaces.DiagnosticKind,
 34         message: []const u8,
 35         loc: ir.Location,
 36     };
 37 
 38     pub const HandleEntry = struct {
 39         slice: []u8,
 40         valid: bool = true,
 41     };
 42 
 43     pub const ValueFrame = struct {
 44         values: std.AutoHashMap(*ir.Value, ir.Attribute),
 45         inherits_parent: bool = false,
 46     };
 47 
 48     pub const EvalError = error{
 49         UnknownEffect,
 50         EffectViolation,
 51         LocationViolation,
 52         DeviceLocationForbidden,
 53         UnifiedLocationForbidden,
 54         BranchQuotaExceeded,
 55         IterationQuotaExceeded,
 56         RecursionDepthExceeded,
 57         InvalidOperand,
 58         InvalidConstant,
 59         DivisionByZero,
 60         InvalidPredicate,
 61         InvalidShiftAmount,
 62         InvalidSize,
 63         NegativeSize,
 64         InvalidCondition,
 65         UnsupportedOperation,
 66         EvaluationFailed,
 67         YieldMissingOperand,
 68         NoYield,
 69         ValueNotFound,
 70         InvalidHandle,
 71         HandleAlreadyDropped,
 72         BorrowOfInvalidHandle,
 73         MoveOfInvalidHandle,
 74         OutOfMemory,
 75     };
 76 
 77     pub fn init(allocator: std.mem.Allocator, ir_ctx: *ir.Context) Evaluator {
 78         return .{
 79             .allocator = allocator,
 80             .ir_ctx = ir_ctx,
 81             .comptime_gpa = .init(allocator),
 82             .current_loc = ir.Location.getUnknown(),
 83             .frames = .empty,
 84             .symbol_table = ir.SymbolTable.init(allocator),
 85             .handle_table = .empty,
 86             .diagnostics = .empty,
 87             .rewrite_builders = .empty,
 88         };
 89     }
 90 
 91     pub fn initMeta(allocator: std.mem.Allocator, ir_ctx: *ir.Context) Evaluator {
 92         return init(allocator, ir_ctx);
 93     }
 94 
 95     pub fn deinit(self: *Evaluator) void {
 96         self.discardRewriteBuilders();
 97         for (self.frames.items) |*frame| {
 98             frame.values.deinit();
 99         }
100         self.frames.deinit(self.allocator);
101         self.symbol_table.deinit();
102         for (self.handle_table.items) |h| {
103             self.comptime_gpa.allocator().free(h.slice);
104         }
105         self.handle_table.deinit(self.allocator);
106         for (self.diagnostics.items) |diag| {
107             self.allocator.free(@constCast(diag.message));
108         }
109         self.diagnostics.deinit(self.allocator);
110         _ = self.comptime_gpa.deinit();
111     }
112 
113     pub fn getDiagnostics(self: *const Evaluator) []const Diagnostic {
114         return self.diagnostics.items;
115     }
116 
117     pub fn clearDiagnostics(self: *Evaluator) void {
118         for (self.diagnostics.items) |diag| {
119             self.allocator.free(@constCast(diag.message));
120         }
121         self.diagnostics.clearRetainingCapacity();
122     }
123 
124     pub fn evaluate(self: *Evaluator, op: *ir.Operation) EvalError!ir.Attribute {
125         self.current_loc = op.location;
126         const num_operands = op.getNumOperands();
127         const iface = op.interface(interfaces.Evaluatable) orelse {
128             emitUnsupportedDiagnostic(self, op, num_operands, "missing Evaluatable interface");
129             return error.UnsupportedOperation;
130         };
131         if (!iface.call(.canEval, .{})) {
132             emitUnsupportedDiagnostic(self, op, num_operands, "canEval returned false");
133             return error.UnsupportedOperation;
134         }
135 
136         var operand_stack: [16]ir.Attribute = undefined;
137         var operand_attrs: []ir.Attribute = operand_stack[0..@min(num_operands, operand_stack.len)];
138         var heap_operands: ?[]ir.Attribute = null;
139         if (num_operands > operand_stack.len) {
140             const allocated = self.allocator.alloc(ir.Attribute, num_operands) catch |err| {
141                 emitUnsupportedDiagnostic(self, op, num_operands, "failed to allocate operand buffer");
142                 return err;
143             };
144             heap_operands = allocated;
145             operand_attrs = allocated;
146         }
147         defer if (heap_operands) |buf| self.allocator.free(buf);
148 
149         for (0..num_operands) |i| {
150             const operand = op.getOperand(@intCast(i)) orelse return error.InvalidOperand;
151             operand_attrs[i] = if (self.getValue(operand)) |attr| attr else resolved_attr: {
152                 if (operand.getDefiningOp()) |op_ptr| {
153                     const defining_op: *ir.Operation = @ptrCast(@alignCast(op_ptr));
154                     const result = try self.evaluate(defining_op);
155                     try self.registerResults(defining_op, result);
156                     if (self.getValue(operand)) |attr| break :resolved_attr attr;
157                 }
158                 return error.ValueNotFound;
159             };
160         }
161 
162         const ctx = self.buildEvalContext();
163         const result = iface.call(.evaluate, .{ operand_attrs[0..num_operands], &ctx }) catch |err| {
164             return mapEvalInterfaceError(err);
165         };
166 
167         return result;
168     }
169 
170     fn enterCall(self: *Evaluator) !void {
171         if (self.recursion_depth >= self.max_recursion) return error.RecursionDepthExceeded;
172         self.recursion_depth += 1;
173     }
174 
175     fn exitCall(self: *Evaluator) void {
176         if (self.recursion_depth > 0) {
177             self.recursion_depth -= 1;
178         }
179     }
180 
181     fn addDiagnostic(self: *Evaluator, kind: interfaces.DiagnosticKind, message: []const u8) !void {
182         const owned_message = try self.allocator.dupe(u8, message);
183         try self.diagnostics.append(self.allocator, .{
184             .kind = kind,
185             .message = owned_message,
186             .loc = self.current_loc,
187         });
188     }
189 
190     fn ensureFrame(self: *Evaluator) !*ValueFrame {
191         if (self.frames.items.len == 0) {
192             try self.frames.append(self.allocator, .{
193                 .values = std.AutoHashMap(*ir.Value, ir.Attribute).init(self.allocator),
194             });
195         }
196         return &self.frames.items[self.frames.items.len - 1];
197     }
198 
199     fn pushFrame(self: *Evaluator, inherits_parent: bool) !void {
200         try self.frames.append(self.allocator, .{
201             .inherits_parent = inherits_parent,
202             .values = std.AutoHashMap(*ir.Value, ir.Attribute).init(self.allocator),
203         });
204     }
205 
206     fn popFrame(self: *Evaluator) void {
207         var frame = self.frames.pop() orelse return;
208         frame.values.deinit();
209     }
210 
211     /// A structured region reads lexical parents, but never crosses a call.
212     pub fn getValue(self: *Evaluator, value: *ir.Value) ?ir.Attribute {
213         var remaining = self.frames.items.len;
214         while (remaining > 0) {
215             remaining -= 1;
216             const frame = &self.frames.items[remaining];
217             if (frame.values.get(value)) |attr| return attr;
218             if (!frame.inherits_parent) break;
219         }
220         return null;
221     }
222 
223     pub fn setValue(self: *Evaluator, value: *ir.Value, attr: ir.Attribute) !void {
224         const frame = try self.ensureFrame();
225         try frame.values.put(value, attr);
226     }
227 
228     pub fn setRootOperation(self: *Evaluator, op: *ir.Operation) !void {
229         self.root_op = op;
230         try self.symbol_table.buildFromOperation(op);
231     }
232 
233     fn finishRewriteBuilders(self: *Evaluator, finalize: bool) void {
234         for (self.rewrite_builders.items) |builder| {
235             if (finalize) {
236                 builder.finalize();
237             }
238             builder.deinit();
239             self.allocator.destroy(builder);
240         }
241         self.rewrite_builders.deinit(self.allocator);
242         self.rewrite_builders = .empty;
243     }
244 
245     pub fn finalizeRewriteBuilders(self: *Evaluator) void {
246         self.finishRewriteBuilders(true);
247     }
248 
249     pub fn discardRewriteBuilders(self: *Evaluator) void {
250         self.finishRewriteBuilders(false);
251     }
252 
253     fn buildEvalContext(self: *Evaluator) interfaces.EvalContext {
254         return .{
255             .state = self,
256             .allocator = self.allocator,
257             .emitDiagnostic = evalContextEmitDiagnostic,
258             .evaluateRegion = evalContextEvaluateRegion,
259             .evaluateRegionWithArgs = evalContextEvaluateRegionWithArgs,
260             .evaluateSymbol = evalContextEvaluateSymbol,
261             .consumeBranchFuel = evalContextConsumeBranchFuel,
262             .consumeIterationFuel = evalContextConsumeIterationFuel,
263             .allocHandle = evalContextAllocHandle,
264             .borrowHandle = evalContextBorrowHandle,
265             .borrowMutHandle = evalContextBorrowMutHandle,
266             .moveHandle = evalContextMoveHandle,
267             .dropHandle = evalContextDropHandle,
268             .createRewriteBuilder = evalContextCreateRewriteBuilder,
269         };
270     }
271 
272     fn evaluateRegionInternal(self: *Evaluator, region: *ir.Region) EvalError!ir.Attribute {
273         if (region.blocks.head) |block_opaque| {
274             const block: *ir.Block = @ptrCast(@alignCast(block_opaque));
275             var op_ptr = block.operations.head;
276             while (op_ptr) |o_opaque| {
277                 const op: *ir.Operation = @ptrCast(@alignCast(o_opaque));
278 
279                 const result = try self.evaluate(op);
280 
281                 try self.registerResults(op, result);
282 
283                 if (op.hasTrait("is_terminator")) {
284                     return result;
285                 }
286 
287                 op_ptr = op.next_op;
288             }
289         }
290         return error.NoYield;
291     }
292 
293     fn registerResults(self: *Evaluator, op: *ir.Operation, result: ir.Attribute) EvalError!void {
294         const num_results = op.getNumResults();
295         if (num_results == 0) return;
296         if (num_results == 1) {
297             const res = op.getResult(0) orelse return error.InvalidOperand;
298             try self.setValue(res, result);
299             return;
300         }
301         const iface = result.interface(interfaces.AttributeArrayInterface) orelse return error.InvalidOperand;
302         const count = iface.call(.getCount, .{});
303         if (count != num_results) return error.InvalidOperand;
304         var i: usize = 0;
305         while (i < count) : (i += 1) {
306             const attr = iface.call(.getElement, .{i}) orelse return error.InvalidOperand;
307             const res = op.getResult(@intCast(i)) orelse return error.InvalidOperand;
308             try self.setValue(res, attr);
309         }
310     }
311 
312     fn bindBlockArgs(self: *Evaluator, region: *ir.Region, args: []const ir.Attribute) EvalError!void {
313         const block = region.getEntryBlock() orelse return error.NoYield;
314         if (block.arguments.items.len != args.len) return error.InvalidOperand;
315         for (args, 0..) |arg, i| {
316             const arg_value = block.getArgument(i) orelse return error.InvalidOperand;
317             try self.setValue(arg_value, arg);
318         }
319     }
320 
321     fn evaluateRegionWithArgs(self: *Evaluator, region: *ir.Region, args: []const ir.Attribute) EvalError!ir.Attribute {
322         try self.pushFrame(true);
323         defer self.popFrame();
324         try self.bindBlockArgs(region, args);
325         return try self.evaluateRegionInternal(region);
326     }
327 
328     pub fn evaluateRegion(self: *Evaluator, region: *ir.Region) EvalError!ir.Attribute {
329         return self.evaluateRegionWithArgs(region, &.{});
330     }
331 
332     fn evaluateFunction(self: *Evaluator, func_op: *ir.Operation, args: []const ir.Attribute) EvalError!ir.Attribute {
333         try self.enterCall();
334         defer self.exitCall();
335         const region = func_op.getRegion(0) orelse return error.NoYield;
336         try self.pushFrame(false);
337         defer self.popFrame();
338         try self.bindBlockArgs(region, args);
339         return self.evaluateRegionInternal(region);
340     }
341 
342     pub fn evaluateFunctionOp(self: *Evaluator, func_op: *ir.Operation, args: []const ir.Attribute) EvalError!ir.Attribute {
343         return self.evaluateFunction(func_op, args);
344     }
345 
346     pub fn evaluateSymbol(self: *Evaluator, symbol: []const u8, args: []const ir.Attribute) EvalError!ir.Attribute {
347         if (self.root_op == null) return error.UnsupportedOperation;
348         if (self.symbol_table.lookup(symbol)) |func_op| {
349             return self.evaluateFunction(func_op, args);
350         }
351         return error.UnsupportedOperation;
352     }
353 
354     pub fn evaluateSymbolOptional(
355         self: *Evaluator,
356         symbol: []const u8,
357         args: []const ir.Attribute,
358     ) EvalError!?ir.Attribute {
359         if (self.root_op == null) return null;
360         if (self.symbol_table.lookup(symbol)) |func_op| {
361             return try self.evaluateFunction(func_op, args);
362         }
363         return null;
364     }
365 };
366 
367 fn emitUnsupportedDiagnostic(
368     evaluator: *Evaluator,
369     op: *ir.Operation,
370     num_operands: usize,
371     reason: []const u8,
372 ) void {
373     const msg = std.fmt.allocPrint(
374         evaluator.allocator,
375         "cannot evaluate '{s}' ({d} operands): {s}",
376         .{ op.name.name, num_operands, reason },
377     ) catch return;
378     defer evaluator.allocator.free(msg);
379     evaluator.addDiagnostic(.error_, msg) catch {};
380 }
381 
382 fn mapEvalInterfaceError(err: interfaces.EvalError) Evaluator.EvalError {
383     return switch (err) {
384         error.UnsupportedOperation => error.UnsupportedOperation,
385         error.UnknownEffect => error.UnknownEffect,
386         error.EffectViolation => error.EffectViolation,
387         error.LocationViolation => error.LocationViolation,
388         error.DivisionByZero => error.DivisionByZero,
389         error.InvalidOperand => error.InvalidOperand,
390         error.InvalidConstant => error.InvalidConstant,
391         error.RequiresDynamicInfo => error.UnsupportedOperation,
392         error.InvalidShiftAmount => error.InvalidShiftAmount,
393         error.InvalidPredicate => error.InvalidPredicate,
394         error.EvaluationFailed => error.EvaluationFailed,
395         error.Overflow => error.UnsupportedOperation,
396         error.RecursionDepthExceeded => error.RecursionDepthExceeded,
397         error.InvalidCondition => error.InvalidCondition,
398         error.InvalidSize => error.InvalidSize,
399         error.NegativeSize => error.NegativeSize,
400         error.InvalidHandle => error.InvalidHandle,
401         error.HandleAlreadyDropped => error.HandleAlreadyDropped,
402         error.BorrowOfInvalidHandle => error.BorrowOfInvalidHandle,
403         error.MoveOfInvalidHandle => error.MoveOfInvalidHandle,
404         error.BranchQuotaExceeded => error.BranchQuotaExceeded,
405         error.IterationQuotaExceeded => error.IterationQuotaExceeded,
406         error.DeviceLocationForbidden => error.DeviceLocationForbidden,
407         error.UnifiedLocationForbidden => error.UnifiedLocationForbidden,
408         error.YieldMissingOperand => error.YieldMissingOperand,
409         error.NoYield => error.NoYield,
410         error.ValueNotFound => error.ValueNotFound,
411         error.OutOfMemory => error.OutOfMemory,
412     };
413 }
414 
415 fn mapEvalError(err: Evaluator.EvalError) interfaces.EvalError {
416     return switch (err) {
417         error.UnknownEffect => error.UnknownEffect,
418         error.EffectViolation => error.EffectViolation,
419         error.LocationViolation => error.LocationViolation,
420         error.DeviceLocationForbidden => error.DeviceLocationForbidden,
421         error.UnifiedLocationForbidden => error.UnifiedLocationForbidden,
422         error.BranchQuotaExceeded => error.BranchQuotaExceeded,
423         error.IterationQuotaExceeded => error.IterationQuotaExceeded,
424         error.RecursionDepthExceeded => error.RecursionDepthExceeded,
425         error.InvalidOperand => error.InvalidOperand,
426         error.InvalidConstant => error.InvalidConstant,
427         error.DivisionByZero => error.DivisionByZero,
428         error.InvalidPredicate => error.InvalidPredicate,
429         error.InvalidShiftAmount => error.InvalidShiftAmount,
430         error.InvalidSize => error.InvalidSize,
431         error.NegativeSize => error.NegativeSize,
432         error.InvalidCondition => error.InvalidCondition,
433         error.UnsupportedOperation => error.UnsupportedOperation,
434         error.EvaluationFailed => error.EvaluationFailed,
435         error.YieldMissingOperand => error.YieldMissingOperand,
436         error.NoYield => error.NoYield,
437         error.ValueNotFound => error.ValueNotFound,
438         error.InvalidHandle => error.InvalidHandle,
439         error.HandleAlreadyDropped => error.HandleAlreadyDropped,
440         error.BorrowOfInvalidHandle => error.BorrowOfInvalidHandle,
441         error.MoveOfInvalidHandle => error.MoveOfInvalidHandle,
442         error.OutOfMemory => error.OutOfMemory,
443     };
444 }
445 
446 fn evalContextEmitDiagnostic(
447     state: *anyopaque,
448     kind: interfaces.DiagnosticKind,
449     message: []const u8,
450 ) interfaces.EvalError!void {
451     const evaluator: *Evaluator = @ptrCast(@alignCast(state));
452     evaluator.addDiagnostic(kind, message) catch return error.OutOfMemory;
453 }
454 
455 fn evalContextEvaluateRegion(state: *anyopaque, region_opaque: *const anyopaque) interfaces.EvalError!ir.Attribute {
456     const evaluator: *Evaluator = @ptrCast(@alignCast(state));
457     const region_const: *const ir.Region = @ptrCast(@alignCast(region_opaque));
458     const region = @constCast(region_const);
459     return evaluator.evaluateRegion(region) catch |err| return mapEvalError(err);
460 }
461 
462 fn evalContextEvaluateRegionWithArgs(
463     state: *anyopaque,
464     region_opaque: *const anyopaque,
465     args: []const ir.Attribute,
466 ) interfaces.EvalError!ir.Attribute {
467     const evaluator: *Evaluator = @ptrCast(@alignCast(state));
468     const region_const: *const ir.Region = @ptrCast(@alignCast(region_opaque));
469     const region = @constCast(region_const);
470     return evaluator.evaluateRegionWithArgs(region, args) catch |err| return mapEvalError(err);
471 }
472 
473 fn evalContextEvaluateSymbol(
474     state: *anyopaque,
475     symbol: []const u8,
476     args: []const ir.Attribute,
477 ) interfaces.EvalError!ir.Attribute {
478     const evaluator: *Evaluator = @ptrCast(@alignCast(state));
479     return evaluator.evaluateSymbol(symbol, args) catch |err| return mapEvalError(err);
480 }
481 
482 fn evalContextConsumeBranchFuel(state: *anyopaque) interfaces.EvalError!void {
483     const evaluator: *Evaluator = @ptrCast(@alignCast(state));
484     if (evaluator.branch_fuel == 0) return error.BranchQuotaExceeded;
485     evaluator.branch_fuel -= 1;
486 }
487 
488 fn evalContextConsumeIterationFuel(state: *anyopaque) interfaces.EvalError!void {
489     const evaluator: *Evaluator = @ptrCast(@alignCast(state));
490     if (evaluator.iteration_fuel == 0) return error.IterationQuotaExceeded;
491     evaluator.iteration_fuel -= 1;
492 }
493 
494 fn evalContextAllocHandle(state: *anyopaque, size: i64) interfaces.EvalError!i64 {
495     const evaluator: *Evaluator = @ptrCast(@alignCast(state));
496     if (size < 0) return error.NegativeSize;
497 
498     const slice = evaluator.comptime_gpa.allocator().alloc(u8, @intCast(size)) catch return error.OutOfMemory;
499     errdefer evaluator.comptime_gpa.allocator().free(slice);
500 
501     evaluator.handle_table.append(evaluator.allocator, .{ .slice = slice }) catch return error.OutOfMemory;
502     return @intCast(evaluator.handle_table.items.len - 1);
503 }
504 
505 fn evalContextBorrowHandle(state: *anyopaque, handle: i64) interfaces.EvalError!i64 {
506     const evaluator: *Evaluator = @ptrCast(@alignCast(state));
507     if (handle < 0) return error.InvalidHandle;
508 
509     const idx: usize = @intCast(handle);
510     if (idx >= evaluator.handle_table.items.len) return error.InvalidHandle;
511     if (!evaluator.handle_table.items[idx].valid) return error.BorrowOfInvalidHandle;
512 
513     return @intCast(idx);
514 }
515 
516 fn evalContextBorrowMutHandle(state: *anyopaque, handle: i64) interfaces.EvalError!i64 {
517     const evaluator: *Evaluator = @ptrCast(@alignCast(state));
518     if (handle < 0) return error.InvalidHandle;
519 
520     const idx: usize = @intCast(handle);
521     if (idx >= evaluator.handle_table.items.len) return error.InvalidHandle;
522     if (!evaluator.handle_table.items[idx].valid) return error.BorrowOfInvalidHandle;
523 
524     return @intCast(idx);
525 }
526 
527 fn evalContextMoveHandle(state: *anyopaque, handle: i64) interfaces.EvalError!i64 {
528     const evaluator: *Evaluator = @ptrCast(@alignCast(state));
529     if (handle < 0) return error.InvalidHandle;
530 
531     const idx: usize = @intCast(handle);
532     if (idx >= evaluator.handle_table.items.len) return error.InvalidHandle;
533     if (!evaluator.handle_table.items[idx].valid) return error.MoveOfInvalidHandle;
534 
535     return @intCast(idx);
536 }
537 
538 fn evalContextDropHandle(state: *anyopaque, handle: i64) interfaces.EvalError!void {
539     const evaluator: *Evaluator = @ptrCast(@alignCast(state));
540     if (handle < 0) return error.InvalidHandle;
541 
542     const idx: usize = @intCast(handle);
543     if (idx >= evaluator.handle_table.items.len) return error.InvalidHandle;
544     if (!evaluator.handle_table.items[idx].valid) return error.HandleAlreadyDropped;
545 
546     evaluator.handle_table.items[idx].valid = false;
547 }
548 
549 fn evalContextCreateRewriteBuilder(
550     state: *anyopaque,
551     root: *ir.Operation,
552 ) interfaces.EvalError!*anyopaque {
553     const evaluator: *Evaluator = @ptrCast(@alignCast(state));
554     const builder = evaluator.allocator.create(rewrite_builder.RewriteBuilder) catch return error.OutOfMemory;
555     builder.* = rewrite_builder.RewriteBuilder.init(
556         evaluator.allocator,
557         root.getContext(),
558         root,
559     );
560     evaluator.rewrite_builders.append(evaluator.allocator, builder) catch return error.OutOfMemory;
561     return @ptrCast(builder);
562 }
563 
564 test "Evaluator handles variadic evaluatable ops beyond stack buffer" {
565     const testing = std.testing;
566     const test_dialect = @import("../dialects/fixture/root.zig");
567 
568     var arena = alloc_arena.Arena.init(std.testing.allocator);
569     defer arena.deinit();
570     const allocator = arena.allocator();
571 
572     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
573     defer ctx.deinit(allocator);
574 
575     const loc = ir.Location.getUnknown();
576     const op_name = "test.sum";
577     _ = try ctx.registerOperation(op_name, .{});
578 
579     const SumEval = struct {
580         fn canEval(op_ptr: *const anyopaque) bool {
581             _ = op_ptr;
582             return true;
583         }
584 
585         fn evaluate(
586             op_ptr: *const anyopaque,
587             operands: []const ir.Attribute,
588             eval_ctx: *const interfaces.EvalContext,
589         ) interfaces.EvalError!ir.Attribute {
590             _ = eval_ctx;
591             const op: *const ir.Operation = @ptrCast(@alignCast(op_ptr));
592             const op_ctx = op.getContext();
593 
594             var total: i64 = 0;
595             for (operands) |attr| {
596                 const value = test_dialect.TestDialect.getIntegerValue(attr) orelse return error.InvalidOperand;
597                 total += value;
598             }
599 
600             return test_dialect.TestDialect.getIntegerAttr(op_ctx, total) catch |err| switch (err) {
601                 error.OutOfMemory => error.OutOfMemory,
602                 else => error.UnsupportedOperation,
603             };
604         }
605     };
606 
607     try ctx.registerOperationInterface(
608         op_name,
609         interfaces.Evaluatable.entryFor(SumEval.canEval, SumEval.evaluate),
610     );
611 
612     const result_type = try test_dialect.TestDialect.getI64Type(&ctx);
613     var constants: std.ArrayListUnmanaged(test_dialect.TestDialect.ConstantOp) = .empty;
614     defer constants.deinit(allocator);
615 
616     var evaluator = Evaluator.init(allocator, &ctx);
617     defer evaluator.deinit();
618 
619     const operand_count: usize = 32;
620     var expected_total: i64 = 0;
621     var operand_values = try allocator.alloc(*ir.Value, operand_count);
622     defer allocator.free(operand_values);
623 
624     for (0..operand_count) |i| {
625         const value: i64 = @intCast(i + 1);
626         expected_total += value;
627         var constant = try test_dialect.TestDialect.ConstantOp.create(&ctx, loc, result_type, value);
628         try constants.append(allocator, constant);
629         operand_values[i] = constant.getResult();
630 
631         const attr = try test_dialect.TestDialect.getIntegerAttr(&ctx, value);
632         try evaluator.setValue(operand_values[i], attr);
633     }
634 
635     var builder = ir.OperationBuilder.init(&ctx);
636     var state = ir.Operation.State.init(op_name, loc);
637     state.addOperands(operand_values);
638     state.addTypes(&.{result_type});
639     const sum_op = try builder.create(state);
640 
641     const result_attr = try evaluator.evaluate(sum_op);
642     try testing.expectEqual(@as(i64, expected_total), test_dialect.TestDialect.getIntegerValue(result_attr).?);
643     try testing.expectEqual(@as(usize, 0), evaluator.getDiagnostics().len);
644 }
645 
646 test "Evaluator emits diagnostic for unsupported operations" {
647     const testing = std.testing;
648 
649     var arena = alloc_arena.Arena.init(std.testing.allocator);
650     defer arena.deinit();
651     const allocator = arena.allocator();
652 
653     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
654     defer ctx.deinit(allocator);
655     try ctx.allowUnregistered();
656 
657     var evaluator = Evaluator.init(allocator, &ctx);
658     defer evaluator.deinit();
659 
660     const loc = ir.Location.getUnknown();
661     var builder = ir.OperationBuilder.init(&ctx);
662     const op = try builder.create(ir.Operation.State.init("test.unknown_eval", loc));
663 
664     try testing.expectError(error.UnsupportedOperation, evaluator.evaluate(op));
665 
666     const diagnostics = evaluator.getDiagnostics();
667     try testing.expectEqual(@as(usize, 1), diagnostics.len);
668     try testing.expect(std.mem.indexOf(u8, diagnostics[0].message, "test.unknown_eval") != null);
669     try testing.expect(std.mem.indexOf(u8, diagnostics[0].message, "missing Evaluatable interface") != null);
670 }