lib/choir/src/composition/diagnostic/path.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const fixture = @import("../fixture/root.zig");
2 const std = @import("std");
3 const alloc_arena = @import("alloc_arena");
4 const pretty = @import("pretty");
5 const choir = @import("../../root.zig");
6 const composition = @import("../root.zig");
7
8 const Allocator = std.mem.Allocator;
9 const Artifact = choir.backends.artifact.Artifact;
10 const CompositionModule = composition.CompositionModule;
11 const ProductKey = choir.product.ProductKey;
12 const Report = pretty.diagnostic.Report;
13
14 pub fn renderPathAlloc(
15 allocator: Allocator,
16 module: *const CompositionModule,
17 variant_id: composition.CompositionVariantId,
18 call_site_id: composition.CallSiteId,
19 ) ![]u8 {
20 try module.verify();
21 const path = try module.diagnosticPath(variant_id, call_site_id);
22 const caller = module.fragment(path.call_site.caller).?;
23 const caller_export = caller.exportByName(path.call_site.caller_export).?;
24 const runtime_import = caller.importByName(path.call_site.runtime_import).?;
25
26 var arena_state = alloc_arena.Arena.init(allocator);
27 defer arena_state.deinit();
28 const arena = arena_state.allocator();
29 const title = try std.fmt.allocPrint(arena, "composition path: {s}", .{path.call_site.name});
30 var report = try Report.init(arena, title);
31 defer report.deinit();
32
33 try report.section("source");
34 try appendLocation(&report, path.source.*);
35 try report.line("operation {s}", .{path.source.symbol});
36 try report.line("module {s}", .{module.source_module.name});
37
38 try report.section("variant");
39 try report.line("[{d}]", .{path.variant.id.value});
40 try appendAddress(&report, "target", path.variant.choice.target);
41 try appendAddress(&report, "policy", path.variant.choice.policy);
42
43 try report.section("partition");
44 try report.line("[{d}] {s}", .{ path.partition.id.value, path.partition.name });
45 try appendAddress(&report, "semantic product", path.partition.product);
46 try report.line("effect {s}", .{@tagName(path.partition.effect)});
47 for (path.partition.inputs) |id| try appendBoundary(&report, arena, module, "input", id);
48 for (path.partition.outputs) |id| try appendBoundary(&report, arena, module, "output", id);
49
50 try report.section("pipeline");
51 try appendProduct(&report, "pipeline input", path.pipeline_input);
52 try report.line("pipeline {s}", .{@tagName(path.fragment.pipeline)});
53
54 try report.section("fragment");
55 try report.line("[{d}] {s}", .{ path.fragment.id.value, @tagName(path.fragment.pipeline) });
56 try appendExport(&report, path.fragment_export.*);
57 for (path.fragment.exports) |*export_value| {
58 if (export_value != path.fragment_export) try appendExport(&report, export_value.*);
59 }
60 for (path.fragment.imports) |import_value| try appendImport(&report, import_value);
61
62 try report.section("call site");
63 try report.line("[{d}] {s}", .{ path.call_site.id.value, path.call_site.name });
64 try report.line(
65 "route [{d}].{s} -> {s} -> [{d}].{s} (ABI v{d})",
66 .{
67 path.call_site.caller.value,
68 path.call_site.caller_export,
69 path.call_site.runtime_import,
70 path.call_site.callee.value,
71 path.call_site.callee_export,
72 path.call_site.abi_version,
73 },
74 );
75 try report.line("caller export {s} -> {s}", .{ caller_export.name, caller_export.symbol });
76 try appendImport(&report, runtime_import.*);
77 for (path.call_site.inputs) |id| try appendBoundary(&report, arena, module, "input", id);
78 for (path.call_site.outputs) |id| try appendBoundary(&report, arena, module, "output", id);
79
80 try report.section("artifact");
81 try appendArtifact(&report, path.artifact);
82
83 const rendered = try report.renderAlloc(.{ .width = 100 });
84 return try allocator.dupe(u8, rendered);
85 }
86
87 fn appendLocation(report: *Report, provenance: composition.Provenance) !void {
88 if (provenance.line == 0) {
89 try report.line("{s}", .{provenance.path});
90 } else if (provenance.column == 0) {
91 try report.line("{s}:{d}", .{ provenance.path, provenance.line });
92 } else {
93 try report.line("{s}:{d}:{d}", .{ provenance.path, provenance.line, provenance.column });
94 }
95 }
96
97 fn appendAddress(report: *Report, label: []const u8, address: choir.product.ProductRef) !void {
98 try report.line("{s} {s}/{s}/{s} [{s}]", .{
99 label, address.producer, address.source, address.stage, address.variant,
100 });
101 }
102
103 fn appendProduct(report: *Report, label: []const u8, key: ProductKey) !void {
104 try appendAddress(report, label, key.ref);
105 try report.line("content bucket 0x{x:0>16}; exact record {d} bytes", .{
106 key.fingerprint(), key.record.bytes().len,
107 });
108 }
109
110 fn appendBoundary(
111 report: *Report,
112 arena: Allocator,
113 module: *const CompositionModule,
114 role: []const u8,
115 id: composition.BoundaryId,
116 ) !void {
117 const boundary = module.source_module.boundary(id).?;
118 const shape = try shapeAlloc(arena, boundary);
119 try report.line(
120 "{s} [{d}] {s}: {s}, {d} bytes, {s}, {s}",
121 .{
122 role,
123 boundary.id.value,
124 boundary.name,
125 shape,
126 boundary.byte_size,
127 @tagName(boundary.access),
128 @tagName(boundary.ownership),
129 },
130 );
131 switch (boundary.alias) {
132 .disjoint => try report.line("{s} alias disjoint", .{role}),
133 .boundary => |alias| try report.line("{s} aliases boundary [{d}]", .{ role, alias.value }),
134 }
135 try appendNamedLocation(report, role, boundary.provenance);
136 }
137
138 fn appendNamedLocation(report: *Report, label: []const u8, provenance: composition.Provenance) !void {
139 if (provenance.line == 0) {
140 try report.line("{s} source {s}", .{ label, provenance.path });
141 } else if (provenance.column == 0) {
142 try report.line("{s} source {s}:{d}", .{ label, provenance.path, provenance.line });
143 } else {
144 try report.line("{s} source {s}:{d}:{d}", .{ label, provenance.path, provenance.line, provenance.column });
145 }
146 }
147
148 fn shapeAlloc(allocator: Allocator, boundary: *const composition.Boundary) ![]u8 {
149 var output = std.Io.Writer.Allocating.init(allocator);
150 defer output.deinit();
151 try output.writer.print("{s}", .{@tagName(boundary.element_type)});
152 for (boundary.dimensions) |dimension| try output.writer.print("[{d}]", .{dimension});
153 return try output.toOwnedSlice();
154 }
155
156 fn appendExport(report: *Report, export_value: composition.Export) !void {
157 try report.line(
158 "export {s} -> {s} (ABI v{d})",
159 .{ export_value.name, export_value.symbol, export_value.abi_version },
160 );
161 }
162
163 fn appendImport(report: *Report, import_value: composition.Import) !void {
164 try report.line(
165 "{s} import {s} -> {s} (ABI v{d})",
166 .{ @tagName(import_value.kind), import_value.name, import_value.symbol, import_value.abi_version },
167 );
168 }
169
170 fn appendArtifact(report: *Report, artifact: *const Artifact) !void {
171 try report.line("{s}", .{@tagName(artifact.metadata.kind)});
172 try report.line(
173 "resources {d}, buffers {d}, relocations {d}",
174 .{ choir.backends.artifact.resource.count(artifact.*), artifact.payload.buffers.items.len, artifact.linkage.relocations.items.len },
175 );
176 const target = artifact.metadata.target;
177 try report.line("target {s}", .{@tagName(target.architecture)});
178 if (target.triple) |value| try report.line("triple {s}", .{value});
179 if (target.vendor) |value| try report.line("vendor {s}", .{value});
180 if (target.os) |value| try report.line("operating system {s}", .{value});
181 if (target.environment) |value| try report.line("environment {s}", .{value});
182 if (target.cpu) |value| try report.line("CPU {s}", .{value});
183 for (target.features) |feature| try report.line("feature {s}", .{feature});
184 const artifact_abi = artifact.metadata.abi;
185 if (artifact_abi.name) |value| try report.line("ABI {s}", .{value});
186 if (artifact_abi.calling_convention) |value| try report.line("calling convention {s}", .{value});
187 if (artifact_abi.object_format) |value| try report.line("object format {s}", .{value});
188 if (artifact_abi.pointer_width_bits) |value| try report.line("pointer width {d} bits", .{value});
189 if (artifact_abi.endianness) |value| try report.line("endianness {s}", .{@tagName(value)});
190 for (artifact.linkage.provided_symbols.items) |symbol| {
191 try report.line("provides {s} ({s}, {s})", .{ symbol.name, @tagName(symbol.kind), @tagName(symbol.binding) });
192 }
193 for (artifact.linkage.required_symbols.items) |symbol| {
194 try report.line("requires {s} ({s}, {s})", .{ symbol.name, @tagName(symbol.kind), @tagName(symbol.binding) });
195 }
196 }
197
198 test "composition path diagnostic renders a deterministic human witness" {
199 const records = try fixture.Records.init(std.testing.allocator);
200 defer records.deinit();
201 const allocator = std.testing.allocator;
202
203 var partitioned = try composition.PartitionedModule.init(allocator, "hybrid.score");
204 defer partitioned.deinit();
205 try partitioned.addBoundary(.{ .id = .{ .value = 1 }, .name = "x", .element_type = .f32, .dimensions = &.{8}, .byte_size = 32, .access = .read_write, .ownership = .borrowed, .alias = .disjoint, .provenance = .{ .path = "score.chic", .symbol = "x", .line = 6, .column = 8 } });
206 try partitioned.addBoundary(.{ .id = .{ .value = 2 }, .name = "y", .element_type = .f32, .dimensions = &.{8}, .byte_size = 32, .access = .write, .ownership = .produced, .alias = .disjoint, .provenance = .{ .path = "score.chic", .symbol = "y", .line = 6, .column = 12 } });
207 try partitioned.addPartition(.{ .id = .{ .value = 10 }, .name = "score/host", .pipeline = .choir, .outputs = &.{.{ .value = 1 }}, .product = .{ .producer = "composition-fixture", .source = "10", .stage = "host.input", .variant = "default" }, .effect = .none, .provenance = .{ .path = "score.chic", .symbol = "score", .line = 4, .column = 1 } });
208 try partitioned.addPartition(.{ .id = .{ .value = 11 }, .name = "score/affine", .pipeline = .accy, .inputs = &.{.{ .value = 1 }}, .outputs = &.{.{ .value = 2 }}, .product = .{ .producer = "composition-fixture", .source = "11", .stage = "hybrid.input", .variant = "default" }, .effect = .unknown, .provenance = .{ .path = "score.chic", .symbol = "affine8", .line = 6, .column = 3 } });
209 try partitioned.addCallSite(.{ .id = .{ .value = 30 }, .name = "score/affine", .caller = .{ .value = 10 }, .callee = .{ .value = 11 }, .inputs = &.{.{ .value = 1 }}, .outputs = &.{.{ .value = 2 }}, .provenance = .{ .path = "score.chic", .symbol = "affine8", .line = 6, .column = 3 } });
210
211 var host_artifact = try choir.backends.artifact.objectFileArtifact(
212 allocator,
213 .{ .architecture = .x86_64 },
214 .{ .name = "sysv", .pointer_width_bits = 64, .endianness = .little },
215 "score",
216 &.{ 0, 0, 0, 0 },
217 &.{.{ .offset = 0, .symbol = composition.abi.invoke_symbol, .kind = .call }},
218 );
219 defer host_artifact.deinit();
220 var accy_artifact = try choir.backends.artifact.objectFileArtifact(
221 allocator,
222 .{ .architecture = .x86_64, .triple = "x86_64-unknown-linux-gnu" },
223 .{ .name = "sysv", .calling_convention = "c", .object_format = "elf", .pointer_width_bits = 64, .endianness = .little },
224 "affine8",
225 &.{ 1, 2, 3, 4 },
226 &.{},
227 );
228 defer accy_artifact.deinit();
229
230 var module = try composition.CompositionModule.init(allocator, &partitioned);
231 defer module.deinit();
232 try module.addFragment(.{
233 .id = .{ .value = 20 },
234 .partition = .{ .value = 10 },
235 .pipeline = .choir,
236 .pipeline_input = try records.key("host.input", "10", "110"),
237
238 .artifacts = &.{host_artifact},
239 .exports = &.{.{ .name = "score", .symbol = "score", .abi_version = composition.abi.version }},
240 .imports = &.{.{ .name = "invoke", .symbol = composition.abi.invoke_symbol, .abi_version = composition.abi.version, .kind = .runtime }},
241 .provenance = .{ .path = "score.chic", .symbol = "score", .line = 4, .column = 1 },
242 });
243 try module.addFragment(.{
244 .id = .{ .value = 21 },
245 .partition = .{ .value = 11 },
246 .pipeline = .accy,
247 .pipeline_input = try records.key("hybrid.input", "11", "111"),
248
249 .artifacts = &.{accy_artifact},
250 .exports = &.{.{ .name = "affine", .symbol = "affine8", .abi_version = composition.abi.version }},
251 .provenance = .{ .path = "score.chic", .symbol = "affine8", .line = 6, .column = 3 },
252 });
253 try module.addVariant(.{
254 .id = .{ .value = 40 },
255 .choice = .{
256 .target = choir.product.productRef("composition-fixture", "1", "score.target", "default"),
257 .policy = choir.product.productRef("composition-fixture", "1", "score.policy", "default"),
258 },
259 .fragments = &.{ .{ .value = 20 }, .{ .value = 21 } },
260 .call_sites = &.{.{
261 .id = .{ .value = 30 },
262 .name = "score/affine",
263 .caller = .{ .value = 20 },
264 .caller_export = "score",
265 .runtime_import = "invoke",
266 .callee = .{ .value = 21 },
267 .callee_export = "affine",
268 .inputs = &.{.{ .value = 1 }},
269 .outputs = &.{.{ .value = 2 }},
270 .abi_version = composition.abi.version,
271 .provenance = .{ .path = "score.chic", .symbol = "affine8", .line = 6, .column = 3 },
272 }},
273 });
274 try module.verify();
275
276 const first = try renderPathAlloc(allocator, &module, .{ .value = 40 }, .{ .value = 30 });
277 defer allocator.free(first);
278 const second = try renderPathAlloc(allocator, &module, .{ .value = 40 }, .{ .value = 30 });
279 defer allocator.free(second);
280 try std.testing.expectEqualStrings(first, second);
281 try std.testing.expectEqualStrings(
282 \\composition path: score/affine
283 \\
284 \\source
285 \\ score.chic:6:3
286 \\ operation affine8
287 \\ module hybrid.score
288 \\
289 \\variant
290 \\ [40]
291 \\ target composition-fixture/1/score.target [default]
292 \\ policy composition-fixture/1/score.policy [default]
293 \\
294 \\partition
295 \\ [11] score/affine
296 \\ semantic product composition-fixture/11/hybrid.input [default]
297 \\ effect unknown
298 \\ input [1] x: f32[8], 32 bytes, read_write, borrowed
299 \\ input alias disjoint
300 \\ input source score.chic:6:8
301 \\ output [2] y: f32[8], 32 bytes, write, produced
302 \\ output alias disjoint
303 \\ output source score.chic:6:12
304 \\
305 \\pipeline
306 \\ pipeline input composition-fixture/11/hybrid.input [default]
307 \\ content bucket 0x5a9441ace2a1e0f6; exact record 141 bytes
308 \\ pipeline accy
309 \\
310 \\fragment
311 \\ [21] accy
312 \\ export affine -> affine8 (ABI v1)
313 \\
314 \\call site
315 \\ [30] score/affine
316 \\ route [20].score -> invoke -> [21].affine (ABI v1)
317 \\ caller export score -> score
318 \\ runtime import invoke -> choir_compose_invoke_v1 (ABI v1)
319 \\ input [1] x: f32[8], 32 bytes, read_write, borrowed
320 \\ input alias disjoint
321 \\ input source score.chic:6:8
322 \\ output [2] y: f32[8], 32 bytes, write, produced
323 \\ output alias disjoint
324 \\ output source score.chic:6:12
325 \\
326 \\artifact
327 \\ object_file
328 \\ resources 1, buffers 1, relocations 0
329 \\ target x86_64
330 \\ triple x86_64-unknown-linux-gnu
331 \\ ABI sysv
332 \\ calling convention c
333 \\ object format elf
334 \\ pointer width 64 bits
335 \\ endianness little
336 \\ provides affine8 (function, external)
337 , first);
338 }