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 }