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 }