lib/choir/src/diagnostics/engine.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const alloc_arena = @import("alloc_arena");
   3 const ir = @import("../core/root.zig");
   4 
   5 pub const Severity = enum {
   6     note,
   7     warning,
   8     remark,
   9     err,
  10 };
  11 
  12 pub const Metadata = struct {
  13     name: []const u8,
  14     value: []const u8,
  15 };
  16 
  17 pub const Note = struct {
  18     location: ir.Location,
  19     message: []const u8,
  20 };
  21 
  22 pub const Diagnostic = struct {
  23     severity: Severity,
  24     location: ir.Location,
  25     message: []const u8,
  26     operation: ?*ir.Operation = null,
  27     error_name: ?[]const u8 = null,
  28     notes: []const Note = &.{},
  29     metadata: []const Metadata = &.{},
  30 
  31     pub fn operationName(self: Diagnostic) ?[]const u8 {
  32         const op = self.operation orelse return null;
  33         return op.name.name;
  34     }
  35 };
  36 
  37 pub fn operationDiagnostic(
  38     op: *ir.Operation,
  39     severity: Severity,
  40     message: []const u8,
  41 ) Diagnostic {
  42     return .{
  43         .severity = severity,
  44         .location = op.getLoc(),
  45         .message = message,
  46         .operation = op,
  47     };
  48 }
  49 
  50 pub const InFlightDiagnostic = struct {
  51     engine: *Engine,
  52     diagnostic: Diagnostic,
  53     notes: std.ArrayListUnmanaged(Note) = .empty,
  54     metadata: std.ArrayListUnmanaged(Metadata) = .empty,
  55     notes_copied: bool = false,
  56     metadata_copied: bool = false,
  57     emitted: bool = false,
  58     owned_message: ?[]u8 = null,
  59 
  60     pub const Error = error{DiagnosticAlreadyEmitted} || std.mem.Allocator.Error;
  61 
  62     pub fn deinit(self: *InFlightDiagnostic, payload_allocator: std.mem.Allocator) void {
  63         if (self.owned_message) |message| {
  64             payload_allocator.free(message);
  65         }
  66         self.notes.deinit(payload_allocator);
  67         self.metadata.deinit(payload_allocator);
  68         self.* = undefined;
  69     }
  70 
  71     pub fn ownMessage(self: *InFlightDiagnostic, message: []u8) void {
  72         self.owned_message = message;
  73         self.diagnostic.message = message;
  74     }
  75 
  76     pub fn attachNote(
  77         self: *InFlightDiagnostic,
  78         payload_allocator: std.mem.Allocator,
  79         message: []const u8,
  80     ) Error!void {
  81         return self.attachNoteAt(payload_allocator, self.diagnostic.location, message);
  82     }
  83 
  84     pub fn attachNoteAt(
  85         self: *InFlightDiagnostic,
  86         payload_allocator: std.mem.Allocator,
  87         location: ir.Location,
  88         message: []const u8,
  89     ) Error!void {
  90         try self.requireMutable();
  91         try self.ensureNotesMutable(payload_allocator);
  92         try self.notes.append(payload_allocator, .{
  93             .location = location,
  94             .message = message,
  95         });
  96     }
  97 
  98     pub fn addMetadata(
  99         self: *InFlightDiagnostic,
 100         payload_allocator: std.mem.Allocator,
 101         name: []const u8,
 102         value: []const u8,
 103     ) Error!void {
 104         try self.requireMutable();
 105         try self.ensureMetadataMutable(payload_allocator);
 106         try self.metadata.append(payload_allocator, .{
 107             .name = name,
 108             .value = value,
 109         });
 110     }
 111 
 112     pub fn emit(self: *InFlightDiagnostic) !HandlerResult {
 113         self.emitted = true;
 114         var diagnostic = self.diagnostic;
 115         if (self.notes_copied) {
 116             diagnostic.notes = self.notes.items;
 117         }
 118         if (self.metadata_copied) {
 119             diagnostic.metadata = self.metadata.items;
 120         }
 121         return self.engine.report(diagnostic);
 122     }
 123 
 124     fn requireMutable(self: *const InFlightDiagnostic) Error!void {
 125         if (self.emitted) return error.DiagnosticAlreadyEmitted;
 126     }
 127 
 128     fn ensureNotesMutable(
 129         self: *InFlightDiagnostic,
 130         payload_allocator: std.mem.Allocator,
 131     ) Error!void {
 132         if (self.notes_copied) return;
 133         try self.notes.appendSlice(payload_allocator, self.diagnostic.notes);
 134         self.notes_copied = true;
 135     }
 136 
 137     fn ensureMetadataMutable(
 138         self: *InFlightDiagnostic,
 139         payload_allocator: std.mem.Allocator,
 140     ) Error!void {
 141         if (self.metadata_copied) return;
 142         try self.metadata.appendSlice(payload_allocator, self.diagnostic.metadata);
 143         self.metadata_copied = true;
 144     }
 145 };
 146 
 147 pub fn BoundInFlightDiagnostic(
 148     comptime Context: type,
 149     comptime payloadAllocator: anytype,
 150 ) type {
 151     return struct {
 152         context: *Context,
 153         diagnostic: InFlightDiagnostic,
 154 
 155         const Self = @This();
 156         pub const Error = InFlightDiagnostic.Error;
 157 
 158         pub fn deinit(self: *Self) void {
 159             self.diagnostic.deinit(payloadAllocator(self.context));
 160             self.* = undefined;
 161         }
 162 
 163         pub fn ownMessage(self: *Self, message: []u8) void {
 164             self.diagnostic.ownMessage(message);
 165         }
 166 
 167         pub fn attachNote(self: *Self, message: []const u8) Error!void {
 168             return self.diagnostic.attachNote(payloadAllocator(self.context), message);
 169         }
 170 
 171         pub fn attachNoteAt(
 172             self: *Self,
 173             location: ir.Location,
 174             message: []const u8,
 175         ) Error!void {
 176             return self.diagnostic.attachNoteAt(
 177                 payloadAllocator(self.context),
 178                 location,
 179                 message,
 180             );
 181         }
 182 
 183         pub fn addMetadata(
 184             self: *Self,
 185             name: []const u8,
 186             value: []const u8,
 187         ) Error!void {
 188             return self.diagnostic.addMetadata(
 189                 payloadAllocator(self.context),
 190                 name,
 191                 value,
 192             );
 193         }
 194 
 195         pub fn emit(self: *Self) !HandlerResult {
 196             return self.diagnostic.emit();
 197         }
 198     };
 199 }
 200 
 201 pub const HandlerResult = enum {
 202     consumed,
 203     propagate,
 204 };
 205 
 206 pub const Handler = struct {
 207     context: ?*anyopaque = null,
 208     handle: *const fn (context: ?*anyopaque, diagnostic: *const Diagnostic) anyerror!HandlerResult,
 209 };
 210 
 211 pub const Engine = struct {
 212     handlers: std.ArrayListUnmanaged(HandlerEntry) = .empty,
 213     next_id: HandlerId = 0,
 214 
 215     pub const HandlerId = usize;
 216 
 217     const HandlerEntry = struct {
 218         id: HandlerId,
 219         handler: Handler,
 220     };
 221 
 222     pub fn init() Engine {
 223         return .{};
 224     }
 225 
 226     pub fn deinit(self: *Engine, handler_allocator: std.mem.Allocator) void {
 227         self.handlers.deinit(handler_allocator);
 228         self.* = undefined;
 229     }
 230 
 231     pub fn registerHandler(
 232         self: *Engine,
 233         handler_allocator: std.mem.Allocator,
 234         handler: Handler,
 235     ) !HandlerId {
 236         const id = self.next_id;
 237         const next_id = std.math.add(HandlerId, id, 1) catch return error.HandlerIdExhausted;
 238         try self.handlers.append(handler_allocator, .{ .id = id, .handler = handler });
 239         self.next_id = next_id;
 240         return id;
 241     }
 242 
 243     pub fn eraseHandler(self: *Engine, id: HandlerId) void {
 244         for (self.handlers.items, 0..) |entry, index| {
 245             if (entry.id == id) {
 246                 _ = self.handlers.orderedRemove(index);
 247                 return;
 248             }
 249         }
 250     }
 251 
 252     pub fn emit(
 253         self: *Engine,
 254         diagnostic: Diagnostic,
 255     ) InFlightDiagnostic {
 256         return .{
 257             .engine = self,
 258             .diagnostic = diagnostic,
 259         };
 260     }
 261 
 262     pub fn report(self: *Engine, diagnostic: Diagnostic) !HandlerResult {
 263         if (active_capture) |scope| {
 264             if (scope.engine == self) {
 265                 try scope.buffer.append(diagnostic);
 266                 return .consumed;
 267             }
 268         }
 269         return self.reportHandlers(diagnostic);
 270     }
 271 
 272     pub fn capture(self: *Engine, buffer: *CaptureBuffer) CaptureScope {
 273         return .{
 274             .engine = self,
 275             .buffer = buffer,
 276         };
 277     }
 278 
 279     pub fn replay(self: *Engine, buffer: *const CaptureBuffer) !HandlerResult {
 280         var result: HandlerResult = .propagate;
 281         for (buffer.diagnostics.items) |diagnostic| {
 282             if (try self.reportHandlers(diagnostic) == .consumed) {
 283                 result = .consumed;
 284             }
 285         }
 286         return result;
 287     }
 288 
 289     fn reportHandlers(self: *Engine, diagnostic: Diagnostic) !HandlerResult {
 290         var index = self.handlers.items.len;
 291         while (index > 0) {
 292             index -= 1;
 293             const handler = self.handlers.items[index].handler;
 294             if (try handler.handle(handler.context, &diagnostic) == .consumed) {
 295                 return .consumed;
 296             }
 297         }
 298         return .propagate;
 299     }
 300 };
 301 
 302 threadlocal var active_capture: ?*CaptureScope = null;
 303 
 304 pub const CaptureBuffer = struct {
 305     arena: alloc_arena.Arena,
 306     diagnostics: std.ArrayListUnmanaged(Diagnostic) = .empty,
 307 
 308     pub fn init(allocator: std.mem.Allocator) CaptureBuffer {
 309         return .{ .arena = alloc_arena.Arena.init(allocator) };
 310     }
 311 
 312     pub fn deinit(self: *CaptureBuffer) void {
 313         self.arena.deinit();
 314         self.* = undefined;
 315     }
 316 
 317     pub fn append(self: *CaptureBuffer, diagnostic: Diagnostic) !void {
 318         const allocator = self.arena.allocator();
 319         var copied = diagnostic;
 320         copied.message = try allocator.dupe(u8, diagnostic.message);
 321         if (diagnostic.error_name) |name| {
 322             copied.error_name = try allocator.dupe(u8, name);
 323         }
 324         copied.notes = try cloneNotes(allocator, diagnostic.notes);
 325         copied.metadata = try cloneMetadata(allocator, diagnostic.metadata);
 326         try self.diagnostics.append(allocator, copied);
 327     }
 328 };
 329 
 330 pub const CaptureScope = struct {
 331     engine: *Engine,
 332     buffer: *CaptureBuffer,
 333     previous: ?*CaptureScope = null,
 334 
 335     pub fn enter(self: *CaptureScope) CaptureGuard {
 336         self.previous = active_capture;
 337         active_capture = self;
 338         return .{ .scope = self };
 339     }
 340 
 341     fn exit(self: *CaptureScope) void {
 342         std.debug.assert(active_capture == self);
 343         active_capture = self.previous;
 344         self.previous = null;
 345     }
 346 };
 347 
 348 pub const CaptureGuard = struct {
 349     scope: ?*CaptureScope,
 350 
 351     pub fn deinit(self: *CaptureGuard) void {
 352         if (self.scope) |scope| {
 353             scope.exit();
 354             self.scope = null;
 355         }
 356     }
 357 };
 358 
 359 fn cloneNotes(allocator: std.mem.Allocator, notes: []const Note) ![]const Note {
 360     if (notes.len == 0) return &.{};
 361     const copied = try allocator.alloc(Note, notes.len);
 362     for (notes, copied) |note, *out| {
 363         out.* = .{
 364             .location = note.location,
 365             .message = try allocator.dupe(u8, note.message),
 366         };
 367     }
 368     return copied;
 369 }
 370 
 371 fn cloneMetadata(allocator: std.mem.Allocator, metadata: []const Metadata) ![]const Metadata {
 372     if (metadata.len == 0) return &.{};
 373     const copied = try allocator.alloc(Metadata, metadata.len);
 374     for (metadata, copied) |entry, *out| {
 375         out.* = .{
 376             .name = try allocator.dupe(u8, entry.name),
 377             .value = try allocator.dupe(u8, entry.value),
 378         };
 379     }
 380     return copied;
 381 }
 382 
 383 pub const VerifierFailure = struct {
 384     stage: []const u8,
 385     root: *ir.Operation,
 386     operation: *ir.Operation,
 387     location: ir.Location,
 388     err: anyerror,
 389 
 390     pub fn init(
 391         stage: []const u8,
 392         root: *ir.Operation,
 393         options: ir.VerifyOptions,
 394         err: anyerror,
 395     ) VerifierFailure {
 396         const operation = findFailureOp(root, options) orelse root;
 397         return .{
 398             .stage = stage,
 399             .root = root,
 400             .operation = operation,
 401             .location = operation.getLoc(),
 402             .err = err,
 403         };
 404     }
 405 
 406     pub fn diagnostic(self: *const VerifierFailure) Diagnostic {
 407         return .{
 408             .severity = .err,
 409             .location = self.location,
 410             .message = "Choir verifier failed",
 411             .operation = self.operation,
 412             .error_name = @errorName(self.err),
 413         };
 414     }
 415 
 416     pub fn format(self: *const VerifierFailure, buf: []u8) []const u8 {
 417         return formatVerifierFailure(buf, self.*);
 418     }
 419 };
 420 
 421 pub fn captureVerifierFailure(
 422     stage: []const u8,
 423     root: *ir.Operation,
 424     options: ir.VerifyOptions,
 425     err: anyerror,
 426 ) VerifierFailure {
 427     return VerifierFailure.init(stage, root, options, err);
 428 }
 429 
 430 pub fn formatVerifierFailure(buf: []u8, failure: VerifierFailure) []const u8 {
 431     var loc_buf: [128]u8 = undefined;
 432     const loc_str = formatLocation(&loc_buf, failure.location);
 433 
 434     return std.fmt.bufPrint(
 435         buf,
 436         "Choir verifier failed ({s}): op={s} {s} error={s}",
 437         .{ failure.stage, failure.operation.name.name, loc_str, @errorName(failure.err) },
 438     ) catch "Choir verifier failed";
 439 }
 440 
 441 pub fn findFailureOp(root: *ir.Operation, options: ir.VerifyOptions) ?*ir.Operation {
 442     const shallow = ir.VerifyOptions{
 443         .check_terminators = options.check_terminators,
 444         .require_terminators = options.require_terminators,
 445         .recursive = false,
 446         .check_use_def = options.check_use_def,
 447         .check_local_dominance = options.check_local_dominance,
 448         .check_cfg = options.check_cfg,
 449         .max_depth = options.max_depth,
 450     };
 451     return findFailureOpRecursive(root, shallow);
 452 }
 453 
 454 fn findFailureOpRecursive(op: *ir.Operation, options: ir.VerifyOptions) ?*ir.Operation {
 455     ir.verifyOperationStructure(op, options) catch return op;
 456 
 457     ir.verify.runTraitVerifiers(op) catch return op;
 458 
 459     if (op.getInterface(ir.VerifyOpInterface)) |vtable| {
 460         vtable.verify(@ptrCast(op)) catch return op;
 461     }
 462 
 463     for (op.regions.items) |*region| {
 464         ir.verifyRegionStructure(region, options) catch return op;
 465 
 466         var block_iter = region.getBlocks();
 467         while (block_iter.next()) |block| {
 468             ir.verifyBlockStructure(block, options) catch return op;
 469 
 470             var op_node: ?*anyopaque = block.operations.head;
 471             while (op_node) |node| {
 472                 const child: *ir.Operation = @ptrCast(@alignCast(node));
 473                 if (findFailureOpRecursive(child, options)) |found| return found;
 474                 op_node = child.next_op;
 475             }
 476         }
 477     }
 478 
 479     ir.verify.runRegionTraitVerifiers(op) catch return op;
 480 
 481     if (op.getInterface(ir.VerifyRegionOpInterface)) |vtable| {
 482         vtable.verify(@ptrCast(op)) catch return op;
 483     }
 484 
 485     return null;
 486 }
 487 
 488 fn formatLocation(buf: []u8, loc: ir.Location) []const u8 {
 489     return switch (loc) {
 490         .unknown => "loc(unknown)",
 491         .file => |f| std.fmt.bufPrint(buf, "loc(\"{s}\":{d}:{d})", .{ f.filename, f.line, f.column }) catch "loc(?)",
 492         .file_range => |range| std.fmt.bufPrint(
 493             buf,
 494             "loc(\"{s}\":{d}:{d})",
 495             .{ range.filename, range.start.line, range.start.column },
 496         ) catch "loc(?)",
 497         .name => |n| std.fmt.bufPrint(buf, "loc(\"{s}\")", .{n.name}) catch "loc(?)",
 498         .fused => "loc(fused)",
 499         .call_site => "loc(callsite)",
 500     };
 501 }
 502 
 503 test "findFailureOp localizes a SameTypeOperandsMismatch trait failure to the nested cmp op" {
 504     const testing = std.testing;
 505     const dialects = @import("../dialects/root.zig");
 506     const ArithDialect = dialects.ArithDialect;
 507     const BuiltinDialect = dialects.BuiltinDialect;
 508     const FuncDialect = dialects.FuncDialect;
 509 
 510     var arena = alloc_arena.Arena.init(std.testing.allocator);
 511     defer arena.deinit();
 512     const allocator = arena.allocator();
 513 
 514     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
 515     defer ctx.deinit(allocator);
 516     try ir.dialects.loadDialectSpec(&ctx, dialects.arith.spec);
 517 
 518     const loc = ir.Location.getUnknown();
 519     const i32_type = try ArithDialect.getI32Type(&ctx);
 520     const i64_type = try ArithDialect.getScalarType(&ctx, .i64);
 521     const bool_type = try ArithDialect.getScalarType(&ctx, .bool);
 522 
 523     const module = try BuiltinDialect.ModuleOp.create(&ctx, loc);
 524     const module_block = module.getBodyBlock();
 525 
 526     var func = try FuncDialect.FuncOp.create(&ctx, loc, "bad_cmp", &.{}, &.{bool_type});
 527     try module_block.addOperation(func.op);
 528 
 529     const entry = func.getEntryBlock();
 530     var c1 = try ArithDialect.ConstantOp.createInt(&ctx, loc, i32_type, 1);
 531     try entry.addOperation(c1.op);
 532     var c2 = try ArithDialect.ConstantOp.createInt(&ctx, loc, i64_type, 2);
 533     try entry.addOperation(c2.op);
 534     const bad_cmp = try ArithDialect.CmpOp.create(&ctx, loc, .eq, c1.getResult(), c2.getResult());
 535     try entry.addOperation(bad_cmp.op);
 536     const ret = try FuncDialect.ReturnOp.create(&ctx, loc, &.{bad_cmp.getResult()});
 537     try entry.addOperation(ret.op);
 538 
 539     const failure_op = findFailureOp(module.op, ir.verify.default_options);
 540     try testing.expect(failure_op != null);
 541     try testing.expectEqual(bad_cmp.op, failure_op.?);
 542 }
 543 
 544 test "captureVerifierFailure carries structured verifier fields and stable text" {
 545     const testing = std.testing;
 546     const dialects = @import("../dialects/root.zig");
 547     const ArithDialect = dialects.ArithDialect;
 548     const BuiltinDialect = dialects.BuiltinDialect;
 549     const FuncDialect = dialects.FuncDialect;
 550 
 551     var arena = alloc_arena.Arena.init(std.testing.allocator);
 552     defer arena.deinit();
 553     const allocator = arena.allocator();
 554 
 555     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
 556     defer ctx.deinit(allocator);
 557     try ir.dialects.loadDialectSpec(&ctx, dialects.arith.spec);
 558 
 559     const loc = ir.Location.getFile("bad.tiny", 7, 13);
 560     const i32_type = try ArithDialect.getI32Type(&ctx);
 561     const i64_type = try ArithDialect.getScalarType(&ctx, .i64);
 562     const bool_type = try ArithDialect.getScalarType(&ctx, .bool);
 563 
 564     const module = try BuiltinDialect.ModuleOp.create(&ctx, loc);
 565     const module_block = module.getBodyBlock();
 566 
 567     var func = try FuncDialect.FuncOp.create(&ctx, loc, "bad_cmp", &.{}, &.{bool_type});
 568     try module_block.addOperation(func.op);
 569 
 570     const entry = func.getEntryBlock();
 571     var c1 = try ArithDialect.ConstantOp.createInt(&ctx, loc, i32_type, 1);
 572     try entry.addOperation(c1.op);
 573     var c2 = try ArithDialect.ConstantOp.createInt(&ctx, loc, i64_type, 2);
 574     try entry.addOperation(c2.op);
 575     const bad_cmp = try ArithDialect.CmpOp.create(&ctx, loc, .eq, c1.getResult(), c2.getResult());
 576     try entry.addOperation(bad_cmp.op);
 577     const ret = try FuncDialect.ReturnOp.create(&ctx, loc, &.{bad_cmp.getResult()});
 578     try entry.addOperation(ret.op);
 579 
 580     const failure = captureVerifierFailure(
 581         "backend/test/verify",
 582         module.op,
 583         ir.verify.default_options,
 584         error.SameTypeOperandsMismatch,
 585     );
 586     const diagnostic = failure.diagnostic();
 587 
 588     try testing.expectEqual(Severity.err, diagnostic.severity);
 589     try testing.expectEqual(bad_cmp.op, diagnostic.operation.?);
 590     try testing.expectEqualStrings("arith.cmp", diagnostic.operationName().?);
 591     try testing.expectEqualStrings("SameTypeOperandsMismatch", diagnostic.error_name.?);
 592     try testing.expect(diagnostic.location.eql(loc));
 593 
 594     var buffer: [256]u8 = undefined;
 595     const text = failure.format(&buffer);
 596     try testing.expect(std.mem.indexOf(u8, text, "backend/test/verify") != null);
 597     try testing.expect(std.mem.indexOf(u8, text, "op=arith.cmp") != null);
 598     try testing.expect(std.mem.indexOf(u8, text, "loc(\"bad.tiny\":7:13)") != null);
 599     try testing.expect(std.mem.indexOf(u8, text, "SameTypeOperandsMismatch") != null);
 600 }
 601 
 602 test "Engine invokes newest handler first and respects propagation" {
 603     const testing = std.testing;
 604 
 605     const Counter = struct {
 606         seen: usize = 0,
 607         consumed: usize = 0,
 608 
 609         fn propagate(context: ?*anyopaque, diagnostic: *const Diagnostic) !HandlerResult {
 610             const self: *@This() = @ptrCast(@alignCast(context.?));
 611             self.seen += 1;
 612             try testing.expectEqual(Severity.warning, diagnostic.severity);
 613             return .propagate;
 614         }
 615 
 616         fn consume(context: ?*anyopaque, diagnostic: *const Diagnostic) !HandlerResult {
 617             const self: *@This() = @ptrCast(@alignCast(context.?));
 618             self.seen += 1;
 619             self.consumed += 1;
 620             try testing.expectEqualStrings("choir.test", diagnostic.metadata[0].value);
 621             return .consumed;
 622         }
 623     };
 624 
 625     var engine = Engine.init();
 626     defer engine.deinit(testing.allocator);
 627 
 628     var counter = Counter{};
 629     _ = try engine.registerHandler(testing.allocator, .{ .context = &counter, .handle = Counter.propagate });
 630     const consuming_handler = try engine.registerHandler(testing.allocator, .{ .context = &counter, .handle = Counter.consume });
 631 
 632     const metadata = [_]Metadata{.{ .name = "dialect", .value = "choir.test" }};
 633     const diagnostic = Diagnostic{
 634         .severity = .warning,
 635         .location = ir.Location.getUnknown(),
 636         .message = "example diagnostic",
 637         .metadata = &metadata,
 638     };
 639     const result = try engine.report(diagnostic);
 640 
 641     try testing.expectEqual(HandlerResult.consumed, result);
 642     try testing.expectEqual(@as(usize, 1), counter.seen);
 643     try testing.expectEqual(@as(usize, 1), counter.consumed);
 644 
 645     engine.eraseHandler(consuming_handler);
 646     const propagated = try engine.report(diagnostic);
 647     try testing.expectEqual(HandlerResult.propagate, propagated);
 648     try testing.expectEqual(@as(usize, 2), counter.seen);
 649     try testing.expectEqual(@as(usize, 1), counter.consumed);
 650 }
 651 
 652 test "Context diagnostics reject named segment exhaustion before mutation" {
 653     comptime {
 654         @stardustClaim(
 655             @import("alloc_phase").capacity.witness(@import("../core/context/root.zig").Context, "choir_context_diagnostic_exhaustion"),
 656             null,
 657             null,
 658             null,
 659             null,
 660             null,
 661             null,
 662         );
 663     }
 664 
 665     const testing = std.testing;
 666     const HandlerFn = struct {
 667         fn handle(_: ?*anyopaque, _: *const Diagnostic) !HandlerResult {
 668             return .propagate;
 669         }
 670     };
 671 
 672     var handler_limits = ir.Context.Limits.testing;
 673     handler_limits.diagnostics.handler_bytes = 1;
 674     var handler_context = try ir.Context.init(testing.allocator, handler_limits);
 675     defer handler_context.deinit(testing.allocator);
 676     handler_context.activate();
 677 
 678     const engine = handler_context.getDiagnosticEngine();
 679     const next_id = engine.next_id;
 680     try testing.expectError(error.OutOfMemory, handler_context.registerDiagnosticHandler(.{
 681         .handle = HandlerFn.handle,
 682     }));
 683     try testing.expectEqual(next_id, engine.next_id);
 684     try testing.expectEqual(@as(usize, 0), engine.handlers.items.len);
 685     try testing.expectEqual(
 686         ir.Context.Segment.diagnostic_handlers,
 687         handler_context.exhaustedSegment().?,
 688     );
 689 
 690     var payload_limits = ir.Context.Limits.testing;
 691     payload_limits.diagnostics.payload_bytes = 1;
 692     var payload_context = try ir.Context.init(testing.allocator, payload_limits);
 693     defer payload_context.deinit(testing.allocator);
 694     payload_context.activate();
 695 
 696     var diagnostic = payload_context.emitDiagnostic(.{
 697         .severity = .warning,
 698         .location = ir.Location.getUnknown(),
 699         .message = "bounded",
 700     });
 701     defer diagnostic.deinit();
 702     try testing.expectError(error.OutOfMemory, diagnostic.attachNote("note"));
 703     try testing.expectEqual(@as(usize, 0), diagnostic.diagnostic.notes.items.len);
 704     try testing.expectEqual(
 705         ir.Context.Segment.diagnostic_payloads,
 706         payload_context.exhaustedSegment().?,
 707     );
 708 
 709     payload_context.clearExhaustion();
 710     try testing.expectError(error.OutOfMemory, diagnostic.addMetadata("name", "value"));
 711     try testing.expectEqual(@as(usize, 0), diagnostic.diagnostic.metadata.items.len);
 712     try testing.expectEqual(
 713         ir.Context.Segment.diagnostic_payloads,
 714         payload_context.exhaustedSegment().?,
 715     );
 716 }
 717 
 718 test "Engine capture replays diagnostics after in-flight storage is destroyed" {
 719     comptime {
 720         @stardustClaim(
 721             @import("alloc_phase").capacity.witness(@import("../core/context/root.zig").Context, "choir_context_foreign_ownership"),
 722             null,
 723             null,
 724             null,
 725             null,
 726             null,
 727             null,
 728         );
 729     }
 730 
 731     const testing = std.testing;
 732 
 733     const Recorder = struct {
 734         seen: usize = 0,
 735         severity: Severity = .note,
 736         message: []const u8 = "",
 737         note: []const u8 = "",
 738         metadata_name: []const u8 = "",
 739         metadata_value: []const u8 = "",
 740 
 741         fn handle(context: ?*anyopaque, diagnostic: *const Diagnostic) !HandlerResult {
 742             const self: *@This() = @ptrCast(@alignCast(context.?));
 743             self.seen += 1;
 744             self.severity = diagnostic.severity;
 745             self.message = diagnostic.message;
 746             self.note = diagnostic.notes[0].message;
 747             self.metadata_name = diagnostic.metadata[0].name;
 748             self.metadata_value = diagnostic.metadata[0].value;
 749             return .consumed;
 750         }
 751     };
 752 
 753     var engine = Engine.init();
 754     defer engine.deinit(testing.allocator);
 755 
 756     var recorder = Recorder{};
 757     _ = try engine.registerHandler(testing.allocator, .{
 758         .context = &recorder,
 759         .handle = Recorder.handle,
 760     });
 761 
 762     var buffer = CaptureBuffer.init(testing.allocator);
 763     defer buffer.deinit();
 764 
 765     {
 766         var scope = engine.capture(&buffer);
 767         var guard = scope.enter();
 768         defer guard.deinit();
 769 
 770         var diagnostic = engine.emit(.{
 771             .severity = .warning,
 772             .location = ir.Location.getUnknown(),
 773             .message = "borrowed",
 774         });
 775         defer diagnostic.deinit(testing.allocator);
 776 
 777         const owned = try testing.allocator.dupe(u8, "owned diagnostic");
 778         diagnostic.ownMessage(owned);
 779         try diagnostic.attachNote(testing.allocator, "captured note");
 780         try diagnostic.addMetadata(testing.allocator, "owner", "capture");
 781 
 782         try testing.expectEqual(HandlerResult.consumed, try diagnostic.emit());
 783     }
 784 
 785     try testing.expectEqual(@as(usize, 0), recorder.seen);
 786     try testing.expectEqual(HandlerResult.consumed, try engine.replay(&buffer));
 787     try testing.expectEqual(@as(usize, 1), recorder.seen);
 788     try testing.expectEqual(Severity.warning, recorder.severity);
 789     try testing.expectEqualStrings("owned diagnostic", recorder.message);
 790     try testing.expectEqualStrings("captured note", recorder.note);
 791     try testing.expectEqualStrings("owner", recorder.metadata_name);
 792     try testing.expectEqualStrings("capture", recorder.metadata_value);
 793 }
 794 
 795 test "operationDiagnostic anchors message to operation and location" {
 796     const testing = std.testing;
 797     const loc = ir.Location.getName("anchor", null);
 798 
 799     var ctx = try ir.Context.init(testing.allocator, ir.Context.Limits.testing);
 800     defer ctx.deinit(testing.allocator);
 801     try ctx.allowUnregistered();
 802 
 803     const op = try ctx.createOperation(ir.Operation.State.init("test.anchor", loc));
 804     const diagnostic = operationDiagnostic(op, .note, "attached to operation");
 805 
 806     try testing.expectEqual(Severity.note, diagnostic.severity);
 807     try testing.expect(diagnostic.location.eql(loc));
 808     try testing.expectEqual(op, diagnostic.operation.?);
 809     try testing.expectEqualStrings("test.anchor", diagnostic.operationName().?);
 810 }
 811 
 812 test "InFlightDiagnostic attaches notes and metadata before reporting" {
 813     const testing = std.testing;
 814     const loc = ir.Location.getFile("emit.tiny", 3, 5);
 815     const note_loc = ir.Location.getFile("emit.tiny", 8, 2);
 816 
 817     const Recorder = struct {
 818         seen: usize = 0,
 819         severity: Severity = .note,
 820         message: []const u8 = "",
 821         note_count: usize = 0,
 822         first_note_location: ir.Location = ir.Location.getUnknown(),
 823         first_note_message: []const u8 = "",
 824         second_note_location: ir.Location = ir.Location.getUnknown(),
 825         metadata_name: []const u8 = "",
 826         metadata_value: []const u8 = "",
 827 
 828         fn handle(context: ?*anyopaque, diagnostic: *const Diagnostic) !HandlerResult {
 829             const self: *@This() = @ptrCast(@alignCast(context.?));
 830             self.seen += 1;
 831             self.severity = diagnostic.severity;
 832             self.message = diagnostic.message;
 833             self.note_count = diagnostic.notes.len;
 834             self.first_note_location = diagnostic.notes[0].location;
 835             self.first_note_message = diagnostic.notes[0].message;
 836             self.second_note_location = diagnostic.notes[1].location;
 837             self.metadata_name = diagnostic.metadata[0].name;
 838             self.metadata_value = diagnostic.metadata[0].value;
 839             return .consumed;
 840         }
 841     };
 842 
 843     var ctx = try ir.Context.init(testing.allocator, ir.Context.Limits.testing);
 844     defer ctx.deinit(testing.allocator);
 845     try ctx.allowUnregistered();
 846 
 847     const op = try ctx.createOperation(ir.Operation.State.init("test.emit", loc));
 848 
 849     var recorder = Recorder{};
 850     _ = try ctx.registerDiagnosticHandler(.{
 851         .context = &recorder,
 852         .handle = Recorder.handle,
 853     });
 854 
 855     var diagnostic = op.emitError("bad operation");
 856     defer diagnostic.deinit();
 857     try diagnostic.attachNote("inherits primary location");
 858     try diagnostic.attachNoteAt(note_loc, "uses explicit location");
 859     try diagnostic.addMetadata("dialect", "choir.test");
 860 
 861     const result = try diagnostic.emit();
 862     try testing.expectEqual(HandlerResult.consumed, result);
 863     try testing.expectEqual(@as(usize, 1), recorder.seen);
 864     try testing.expectEqual(Severity.err, recorder.severity);
 865     try testing.expectEqualStrings("bad operation", recorder.message);
 866     try testing.expectEqual(@as(usize, 2), recorder.note_count);
 867     try testing.expect(recorder.first_note_location.eql(loc));
 868     try testing.expectEqualStrings("inherits primary location", recorder.first_note_message);
 869     try testing.expect(recorder.second_note_location.eql(note_loc));
 870     try testing.expectEqualStrings("dialect", recorder.metadata_name);
 871     try testing.expectEqualStrings("choir.test", recorder.metadata_value);
 872     try testing.expectError(error.DiagnosticAlreadyEmitted, diagnostic.attachNote("too late"));
 873 }
 874 
 875 test "Context diagnostic engine receives Operation emit helpers" {
 876     const testing = std.testing;
 877     const loc = ir.Location.getFile("emit.tiny", 3, 5);
 878 
 879     const Recorder = struct {
 880         seen: usize = 0,
 881         severity: Severity = .note,
 882         message: []const u8 = "",
 883         op_name: []const u8 = "",
 884         location: ir.Location = ir.Location.getUnknown(),
 885 
 886         fn handle(context: ?*anyopaque, diagnostic: *const Diagnostic) !HandlerResult {
 887             const self: *@This() = @ptrCast(@alignCast(context.?));
 888             self.seen += 1;
 889             self.severity = diagnostic.severity;
 890             self.message = diagnostic.message;
 891             self.op_name = diagnostic.operationName().?;
 892             self.location = diagnostic.location;
 893             return .consumed;
 894         }
 895     };
 896 
 897     var ctx = try ir.Context.init(testing.allocator, ir.Context.Limits.testing);
 898     defer ctx.deinit(testing.allocator);
 899     try ctx.allowUnregistered();
 900 
 901     const op = try ctx.createOperation(ir.Operation.State.init("test.emit", loc));
 902 
 903     var recorder = Recorder{};
 904     const handler_id = try ctx.registerDiagnosticHandler(.{
 905         .context = &recorder,
 906         .handle = Recorder.handle,
 907     });
 908 
 909     var diagnostic = op.emitError("bad operation");
 910     defer diagnostic.deinit();
 911     const result = try diagnostic.emit();
 912     try testing.expectEqual(HandlerResult.consumed, result);
 913     try testing.expectEqual(@as(usize, 1), recorder.seen);
 914     try testing.expectEqual(Severity.err, recorder.severity);
 915     try testing.expectEqualStrings("bad operation", recorder.message);
 916     try testing.expectEqualStrings("test.emit", recorder.op_name);
 917     try testing.expect(recorder.location.eql(loc));
 918 
 919     var remark_diagnostic = op.emitRemark("reviewed operation");
 920     defer remark_diagnostic.deinit();
 921     const remark = try remark_diagnostic.emit();
 922     try testing.expectEqual(HandlerResult.consumed, remark);
 923     try testing.expectEqual(Severity.remark, recorder.severity);
 924 
 925     ctx.eraseDiagnosticHandler(handler_id);
 926     var warning_diagnostic = op.emitWarning("unhandled warning");
 927     defer warning_diagnostic.deinit();
 928     const propagated = try warning_diagnostic.emit();
 929     try testing.expectEqual(HandlerResult.propagate, propagated);
 930     try testing.expectEqual(@as(usize, 2), recorder.seen);
 931 }
 932 
 933 test "Operation emitOpError prefixes the operation name" {
 934     const testing = std.testing;
 935     const loc = ir.Location.getName("op error", null);
 936 
 937     const Recorder = struct {
 938         message: []const u8 = "",
 939 
 940         fn handle(context: ?*anyopaque, diagnostic: *const Diagnostic) !HandlerResult {
 941             const self: *@This() = @ptrCast(@alignCast(context.?));
 942             self.message = diagnostic.message;
 943             return .consumed;
 944         }
 945     };
 946 
 947     var ctx = try ir.Context.init(testing.allocator, ir.Context.Limits.testing);
 948     defer ctx.deinit(testing.allocator);
 949     try ctx.allowUnregistered();
 950 
 951     const op = try ctx.createOperation(ir.Operation.State.init("test.prefixed", loc));
 952 
 953     var recorder = Recorder{};
 954     _ = try ctx.registerDiagnosticHandler(.{
 955         .context = &recorder,
 956         .handle = Recorder.handle,
 957     });
 958 
 959     var diagnostic = try op.emitOpError("requires one operand");
 960     defer diagnostic.deinit();
 961     try testing.expectEqual(HandlerResult.consumed, try diagnostic.emit());
 962     try testing.expectEqualStrings("'test.prefixed' op requires one operand", recorder.message);
 963 }
 964 
 965 test "Choir named parse localizes a verifier failure and preserves its dump" {
 966     const testing = std.testing;
 967     const dialects = @import("../dialects/root.zig");
 968     var ctx = try ir.Context.init(testing.allocator, ir.Context.Limits.testing);
 969     defer ctx.deinit(testing.allocator);
 970     try ir.dialects.loadDialectSpec(&ctx, dialects.BuiltinDialect.spec);
 971     try ir.dialects.loadDialectSpec(&ctx, dialects.arith.spec);
 972     try ir.dialects.loadDialectSpec(&ctx, dialects.ScfDialect.spec);
 973     const text =
 974         \\builtin.module() {
 975         \\  ^bb0(%0: !arith.index):
 976         \\    %1 = arith.constant() {value = 0.0:f64}: !arith.f64
 977         \\    %7 = scf.for(%0, %0, %0, %1): !arith.f64 {
 978         \\      ^bb1(%4: !arith.index, %5: !arith.f64):
 979         \\        scf.yield(%0)
 980         \\    }
 981         \\}
 982         \\
 983     ;
 984     const name = "/tmp/tiny-d1-location-probe/wrong.choir";
 985     const parsed = try ir.parse.source(&ctx, .{ .name = name, .text = text });
 986     defer parsed.erase();
 987     const expected_dump =
 988         \\builtin.module() {
 989         \\  ^bb0(%0: !arith.index):
 990         \\    %1 = arith.constant() {value = 0:f64} : !arith.f64
 991         \\    %2 = scf.for(%0, %0, %0, %1) : !arith.f64 {
 992         \\      ^bb0(%3: !arith.index, %4: !arith.f64):
 993         \\        scf.yield(%0)
 994         \\    }
 995         \\}
 996         \\
 997     ;
 998     const dumped = try ir.dump.operationAlloc(testing.allocator, parsed);
 999     defer testing.allocator.free(dumped);
1000     try testing.expectEqualStrings(expected_dump, dumped);
1001     const reparsed = try ir.parse.operation(&ctx, dumped);
1002     defer reparsed.erase();
1003     const redumped = try ir.dump.operationAlloc(testing.allocator, reparsed);
1004     defer testing.allocator.free(redumped);
1005     try testing.expectEqualStrings(dumped, redumped);
1006 
1007     try testing.expectError(
1008         error.ScfForYieldTypeMismatch,
1009         ir.verify.verifyOperation(parsed, ir.verify.default_options),
1010     );
1011     const failure = captureVerifierFailure(
1012         "ir.verify.verifyOperation",
1013         parsed,
1014         ir.verify.default_options,
1015         error.ScfForYieldTypeMismatch,
1016     );
1017     try testing.expectEqualStrings("scf.for", failure.operation.name.name);
1018     try testing.expect(failure.location == .file_range);
1019     const range = failure.location.file_range;
1020     try testing.expectEqualStrings(name, range.filename);
1021     try testing.expectEqual(@as(u32, 4), range.start.line);
1022     try testing.expectEqual(@as(u32, 5), range.start.column);
1023     try testing.expectEqualStrings(
1024         "%7 = scf.for(%0, %0, %0, %1): !arith.f64 {",
1025         text[range.start.byte..range.end.byte],
1026     );
1027     try testing.expectEqual(@as(u32, 4), range.end.line);
1028     try testing.expectEqual(@as(u32, 47), range.end.column);
1029     var buffer: [512]u8 = undefined;
1030     try testing.expectEqualStrings(
1031         "Choir verifier failed (ir.verify.verifyOperation): op=scf.for " ++
1032             "loc(\"/tmp/tiny-d1-location-probe/wrong.choir\":4:5) " ++
1033             "error=ScfForYieldTypeMismatch",
1034         failure.format(&buffer),
1035     );
1036 }