lib/choir/src/composition/module/test.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const fixture = @import("../fixture/root.zig");
  2 const std = @import("std");
  3 const subject = @import("root.zig");
  4 const serialization = @import("serialization/test.zig");
  5 const verification = @import("verification/test.zig");
  6 
  7 const CallSiteSpec = subject.CallSiteSpec;
  8 const TargetChoiceSpec = subject.TargetChoiceSpec;
  9 const CompositionVariantSpec = subject.CompositionVariantSpec;
 10 const CompositionModule = subject.CompositionModule;
 11 
 12 test {
 13     std.testing.refAllDecls(subject);
 14     _ = serialization;
 15     _ = verification;
 16 }
 17 
 18 test "composition module verifies artifacts and retains a diagnostic path" {
 19     const records = try fixture.Records.init(std.testing.allocator);
 20     defer records.deinit();
 21     const choir = @import("../../root.zig");
 22     const composition_root = @import("../root.zig");
 23     const abi = composition_root.abi;
 24     const product = choir.product;
 25     const source = composition_root.source;
 26 
 27     var partitioned = try source.PartitionedModule.init(std.testing.allocator, "hybrid.score");
 28     defer partitioned.deinit();
 29     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 = 4 } });
 30     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 } });
 31     try partitioned.addPartition(.{ .id = .{ .value = 10 }, .name = "score/host", .pipeline = .choir, .outputs = &.{.{ .value = 1 }}, .product = .{ .producer = "composition-fixture", .source = "10", .stage = "hybrid.input", .variant = "default" }, .effect = .none, .provenance = .{ .path = "score.chic", .symbol = "score", .line = 4 } });
 32     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 = .none, .provenance = .{ .path = "score.chic", .symbol = "affine8", .line = 6 } });
 33     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 } });
 34 
 35     var host_artifact = try choir.backends.artifact.objectFileArtifact(
 36         std.testing.allocator,
 37         .{ .architecture = .x86_64 },
 38         .{ .name = "sysv", .pointer_width_bits = 64, .endianness = .little },
 39         "score",
 40         &.{ 0, 0, 0, 0 },
 41         &.{.{ .offset = 0, .symbol = abi.invoke_symbol, .kind = .call }},
 42     );
 43     defer host_artifact.deinit();
 44     var accy_artifact = try choir.backends.artifact.objectFileArtifact(
 45         std.testing.allocator,
 46         .{ .architecture = .x86_64 },
 47         .{ .name = "sysv", .pointer_width_bits = 64, .endianness = .little },
 48         "affine8",
 49         &.{ 1, 2, 3, 4 },
 50         &.{},
 51     );
 52     defer accy_artifact.deinit();
 53 
 54     var module = try CompositionModule.init(std.testing.allocator, &partitioned);
 55     defer module.deinit();
 56     try std.testing.expectError(error.ProductIdentityMismatch, module.addFragment(.{
 57         .id = .{ .value = 20 },
 58         .partition = .{ .value = 10 },
 59         .pipeline = .choir,
 60         .pipeline_input = try records.key("hybrid.input", "99", "110"),
 61 
 62         .artifacts = &.{host_artifact},
 63         .exports = &.{.{ .name = "score", .symbol = "score", .abi_version = abi.version }},
 64         .imports = &.{.{ .name = "invoke", .symbol = abi.invoke_symbol, .abi_version = abi.version, .kind = .runtime }},
 65         .provenance = .{ .path = "score.chic", .symbol = "score", .line = 4 },
 66     }));
 67     try module.addFragment(.{
 68         .id = .{ .value = 20 },
 69         .partition = .{ .value = 10 },
 70         .pipeline = .choir,
 71         .pipeline_input = try records.key("hybrid.input", "10", "110"),
 72 
 73         .artifacts = &.{host_artifact},
 74         .exports = &.{.{ .name = "score", .symbol = "score", .abi_version = abi.version }},
 75         .imports = &.{.{ .name = "invoke", .symbol = abi.invoke_symbol, .abi_version = abi.version, .kind = .runtime }},
 76         .provenance = .{ .path = "score.chic", .symbol = "score", .line = 4 },
 77     });
 78     try module.addFragment(.{
 79         .id = .{ .value = 21 },
 80         .partition = .{ .value = 11 },
 81         .pipeline = .accy,
 82         .pipeline_input = try records.key("hybrid.input", "11", "111"),
 83 
 84         .artifacts = &.{accy_artifact},
 85         .exports = &.{.{ .name = "affine", .symbol = "affine8", .abi_version = abi.version }},
 86         .provenance = .{ .path = "score.chic", .symbol = "affine8", .line = 6 },
 87     });
 88     const call_spec = CallSiteSpec{
 89         .id = .{ .value = 30 },
 90         .name = "score/affine",
 91         .caller = .{ .value = 20 },
 92         .caller_export = "score",
 93         .runtime_import = "invoke",
 94         .callee = .{ .value = 21 },
 95         .callee_export = "affine",
 96         .inputs = &.{.{ .value = 1 }},
 97         .outputs = &.{.{ .value = 2 }},
 98         .abi_version = abi.version,
 99         .provenance = .{ .path = "score.chic", .symbol = "affine8", .line = 6 },
100     };
101     const variant_spec = CompositionVariantSpec{
102         .id = .{ .value = 1 },
103         .choice = .{
104             .target = product.productRef("composition-fixture", "1", "hybrid.target", "default"),
105             .policy = product.productRef("composition-fixture", "1", "hybrid.policy", "default"),
106         },
107         .fragments = &.{ .{ .value = 20 }, .{ .value = 21 } },
108         .call_sites = &.{call_spec},
109     };
110     const runtime_symbol = module.fragments.items[0].imports[0].symbol;
111     module.fragments.items[0].imports[0].symbol = "wrong_invoke";
112     defer module.fragments.items[0].imports[0].symbol = runtime_symbol;
113     try std.testing.expectError(error.RuntimeSymbolMismatch, module.addVariant(variant_spec));
114     module.fragments.items[0].imports[0].symbol = runtime_symbol;
115     try module.addVariant(variant_spec);
116     try module.verify();
117     module.fragments.items[1].pipeline_input.ref.source = "99";
118     try std.testing.expectError(error.ProductIdentityMismatch, module.verify());
119     module.fragments.items[1].pipeline_input.ref.source = "11";
120     try module.verify();
121 
122     const path = try module.diagnosticPath(.{ .value = 1 }, .{ .value = 30 });
123     try std.testing.expectEqualStrings("score/affine", path.partition.name);
124     try std.testing.expectEqual(source.Pipeline.accy, path.fragment.pipeline);
125     try std.testing.expectEqualStrings("hybrid.input", path.pipeline_input.ref.stage);
126     try std.testing.expectEqualStrings("affine8", path.fragment_export.symbol);
127     try std.testing.expectEqual(choir.backends.artifact.ArtifactKind.object_file, path.artifact.metadata.kind);
128 
129     const resources = try std.testing.allocator.alloc(choir.bytecode.Resource, module.bytecodeResourceCount());
130     defer std.testing.allocator.free(resources);
131     const written = try module.writeBytecodeResources(resources);
132     try std.testing.expectEqual(@as(usize, 2), written.len);
133 }
134 
135 test "target choices select distinct complete composition variants" {
136     const records = try fixture.Records.init(std.testing.allocator);
137     defer records.deinit();
138     const choir = @import("../../root.zig");
139     const composition_root = @import("../root.zig");
140     const abi = composition_root.abi;
141     const product = choir.product;
142     const source = composition_root.source;
143 
144     var partitioned = try source.PartitionedModule.init(std.testing.allocator, "variants");
145     defer partitioned.deinit();
146     try partitioned.addPartition(.{
147         .id = .{ .value = 1 },
148         .name = "host",
149         .pipeline = .choir,
150         .product = product.productRef("composition-fixture", "1", "variants.host.input", "default"),
151         .effect = .none,
152         .provenance = .{ .path = "variants.chic", .symbol = "host" },
153     });
154     try partitioned.addPartition(.{
155         .id = .{ .value = 2 },
156         .name = "kernel",
157         .pipeline = .accy,
158         .product = product.productRef("composition-fixture", "2", "variants.kernel.input", "default"),
159         .effect = .none,
160         .provenance = .{ .path = "variants.chic", .symbol = "kernel" },
161     });
162     try partitioned.addCallSite(.{
163         .id = .{ .value = 30 },
164         .name = "host/kernel",
165         .caller = .{ .value = 1 },
166         .callee = .{ .value = 2 },
167         .inputs = &.{},
168         .outputs = &.{},
169         .provenance = .{ .path = "variants.chic", .symbol = "kernel" },
170     });
171 
172     var host_x86 = try choir.backends.artifact.objectFileArtifact(
173         std.testing.allocator,
174         .{ .architecture = .x86_64 },
175         .{},
176         "host_x86",
177         &.{1},
178         &.{.{ .offset = 0, .symbol = abi.invoke_symbol, .kind = .call }},
179     );
180     defer host_x86.deinit();
181     var kernel_x86 = try choir.backends.artifact.objectFileArtifact(
182         std.testing.allocator,
183         .{ .architecture = .x86_64 },
184         .{},
185         "kernel_x86",
186         &.{2},
187         &.{},
188     );
189     defer kernel_x86.deinit();
190     var host_arm = try choir.backends.artifact.objectFileArtifact(
191         std.testing.allocator,
192         .{ .architecture = .aarch64 },
193         .{},
194         "host_arm",
195         &.{3},
196         &.{.{ .offset = 0, .symbol = abi.invoke_symbol, .kind = .call }},
197     );
198     defer host_arm.deinit();
199     var kernel_arm = try choir.backends.artifact.objectFileArtifact(
200         std.testing.allocator,
201         .{ .architecture = .aarch64 },
202         .{},
203         "kernel_arm",
204         &.{4},
205         &.{},
206     );
207     defer kernel_arm.deinit();
208 
209     var module = try CompositionModule.init(std.testing.allocator, &partitioned);
210     defer module.deinit();
211     try module.addFragment(.{
212         .id = .{ .value = 10 },
213         .partition = .{ .value = 1 },
214         .pipeline = .choir,
215         .pipeline_input = try records.key("variants.host.input", "1", "101"),
216 
217         .artifacts = &.{host_x86},
218         .exports = &.{.{ .name = "host", .symbol = "host_x86", .abi_version = abi.version }},
219         .imports = &.{.{ .name = "invoke", .symbol = abi.invoke_symbol, .abi_version = abi.version, .kind = .runtime }},
220         .provenance = .{ .path = "variants.chic", .symbol = "host" },
221     });
222     try module.addFragment(.{
223         .id = .{ .value = 11 },
224         .partition = .{ .value = 1 },
225         .pipeline = .choir,
226         .pipeline_input = try records.key("variants.host.input", "1", "101"),
227 
228         .artifacts = &.{host_arm},
229         .exports = &.{.{ .name = "host", .symbol = "host_arm", .abi_version = abi.version }},
230         .imports = &.{.{ .name = "invoke", .symbol = abi.invoke_symbol, .abi_version = abi.version, .kind = .runtime }},
231         .provenance = .{ .path = "variants.chic", .symbol = "host" },
232     });
233     try module.addFragment(.{
234         .id = .{ .value = 20 },
235         .partition = .{ .value = 2 },
236         .pipeline = .accy,
237         .pipeline_input = try records.key("variants.kernel.input", "2", "201"),
238 
239         .artifacts = &.{kernel_x86},
240         .exports = &.{.{ .name = "kernel", .symbol = "kernel_x86", .abi_version = abi.version }},
241         .provenance = .{ .path = "variants.chic", .symbol = "kernel" },
242     });
243     try module.addFragment(.{
244         .id = .{ .value = 21 },
245         .partition = .{ .value = 2 },
246         .pipeline = .accy,
247         .pipeline_input = try records.key("variants.kernel.input", "2", "201"),
248 
249         .artifacts = &.{kernel_arm},
250         .exports = &.{.{ .name = "kernel", .symbol = "kernel_arm", .abi_version = abi.version }},
251         .provenance = .{ .path = "variants.chic", .symbol = "kernel" },
252     });
253 
254     const x86_choice = TargetChoiceSpec{
255         .target = product.productRef("composition-fixture", "x86_64", "variants.target", "default"),
256         .policy = product.productRef("composition-fixture", "prefer-native", "variants.policy", "default"),
257     };
258     const arm_choice = TargetChoiceSpec{
259         .target = product.productRef("composition-fixture", "aarch64", "variants.target", "default"),
260         .policy = x86_choice.policy,
261     };
262     const x86_call = CallSiteSpec{
263         .id = .{ .value = 30 },
264         .name = "host/kernel",
265         .caller = .{ .value = 10 },
266         .caller_export = "host",
267         .runtime_import = "invoke",
268         .callee = .{ .value = 20 },
269         .callee_export = "kernel",
270         .inputs = &.{},
271         .outputs = &.{},
272         .abi_version = abi.version,
273         .provenance = .{ .path = "variants.chic", .symbol = "kernel" },
274     };
275     const arm_call = CallSiteSpec{
276         .id = .{ .value = 30 },
277         .name = "host/kernel",
278         .caller = .{ .value = 11 },
279         .caller_export = "host",
280         .runtime_import = "invoke",
281         .callee = .{ .value = 21 },
282         .callee_export = "kernel",
283         .inputs = &.{},
284         .outputs = &.{},
285         .abi_version = abi.version,
286         .provenance = .{ .path = "variants.chic", .symbol = "kernel" },
287     };
288     const x86_variant = CompositionVariantSpec{
289         .id = .{ .value = 100 },
290         .choice = x86_choice,
291         .fragments = &.{ .{ .value = 10 }, .{ .value = 20 } },
292         .call_sites = &.{x86_call},
293     };
294     try module.addVariant(x86_variant);
295     try std.testing.expectError(error.UnreferencedFragment, module.verify());
296     try std.testing.expectError(error.DuplicateCompositionVariant, module.addVariant(.{
297         .id = x86_variant.id,
298         .choice = arm_choice,
299         .fragments = &.{ .{ .value = 11 }, .{ .value = 21 } },
300         .call_sites = &.{arm_call},
301     }));
302     try std.testing.expectError(error.DuplicateTargetChoice, module.addVariant(.{
303         .id = .{ .value = 101 },
304         .choice = x86_choice,
305         .fragments = &.{ .{ .value = 10 }, .{ .value = 20 } },
306         .call_sites = &.{x86_call},
307     }));
308     try std.testing.expectError(error.MissingPartitionFragment, module.addVariant(.{
309         .id = .{ .value = 101 },
310         .choice = arm_choice,
311         .fragments = &.{.{ .value = 11 }},
312         .call_sites = &.{arm_call},
313     }));
314     try std.testing.expectError(error.DuplicatePartitionFragment, module.addVariant(.{
315         .id = .{ .value = 101 },
316         .choice = arm_choice,
317         .fragments = &.{ .{ .value = 10 }, .{ .value = 11 } },
318         .call_sites = &.{arm_call},
319     }));
320     try std.testing.expectError(error.FragmentOutsideVariant, module.addVariant(.{
321         .id = .{ .value = 101 },
322         .choice = arm_choice,
323         .fragments = &.{ .{ .value = 10 }, .{ .value = 20 } },
324         .call_sites = &.{arm_call},
325     }));
326     try std.testing.expectError(error.EmptyTargetIdentity, module.addVariant(.{
327         .id = .{ .value = 101 },
328         .choice = .{ .target = product.productRef("composition-fixture", "1", "", "default"), .policy = arm_choice.policy },
329         .fragments = &.{ .{ .value = 11 }, .{ .value = 21 } },
330         .call_sites = &.{arm_call},
331     }));
332     try std.testing.expectError(error.EmptyPolicyIdentity, module.addVariant(.{
333         .id = .{ .value = 101 },
334         .choice = .{ .target = arm_choice.target, .policy = product.productRef("composition-fixture", "1", "", "default") },
335         .fragments = &.{ .{ .value = 11 }, .{ .value = 21 } },
336         .call_sites = &.{arm_call},
337     }));
338 
339     try module.addVariant(.{
340         .id = .{ .value = 101 },
341         .choice = arm_choice,
342         .fragments = &.{ .{ .value = 11 }, .{ .value = 21 } },
343         .call_sites = &.{arm_call},
344     });
345     try module.verify();
346     try std.testing.expect(module.fragments.items[0].pipeline_input.eql(module.fragments.items[1].pipeline_input));
347     try std.testing.expect(module.fragments.items[2].pipeline_input.eql(module.fragments.items[3].pipeline_input));
348     const selected_x86 = module.selectVariant(x86_choice) orelse return error.TestExpectedResult;
349     const selected_arm = module.selectVariant(arm_choice) orelse return error.TestExpectedResult;
350     try std.testing.expectEqualSlices(
351         source.FragmentId,
352         &.{ .{ .value = 10 }, .{ .value = 20 } },
353         selected_x86.fragments,
354     );
355     try std.testing.expectEqualSlices(
356         source.FragmentId,
357         &.{ .{ .value = 11 }, .{ .value = 21 } },
358         selected_arm.fragments,
359     );
360     try std.testing.expect(module.selectVariant(.{
361         .target = x86_choice.target,
362         .policy = product.productRef("composition-fixture", "prefer-portable", "variants.policy", "default"),
363     }) == null);
364 
365     {
366         const compatible_input = module.fragments.items[1].pipeline_input;
367         defer module.fragments.items[1].pipeline_input = compatible_input;
368         module.fragments.items[1].pipeline_input = try records.key("variants.host.input", "1", "changed");
369         try std.testing.expectError(error.ConflictingProductKey, module.verify());
370     }
371     try module.verify();
372 }
373 
374 test "composition module survives every allocation failure" {
375     const choir = @import("../../root.zig");
376     const composition_root = @import("../root.zig");
377     const abi = composition_root.abi;
378     const product = choir.product;
379     const source = composition_root.source;
380     const Harness = struct {
381         fn run(allocator: std.mem.Allocator) !void {
382             const records = try fixture.Records.init(allocator);
383             defer records.deinit();
384             var partitioned = try source.PartitionedModule.init(allocator, "allocation");
385             defer partitioned.deinit();
386             try partitioned.addBoundary(.{ .id = .{ .value = 1 }, .name = "input", .element_type = .u8, .dimensions = &.{8}, .byte_size = 8, .access = .read, .ownership = .borrowed, .alias = .disjoint, .provenance = .{ .path = "allocation", .symbol = "input" } });
387             try partitioned.addBoundary(.{ .id = .{ .value = 2 }, .name = "output", .element_type = .u8, .dimensions = &.{8}, .byte_size = 8, .access = .write, .ownership = .produced, .alias = .disjoint, .provenance = .{ .path = "allocation", .symbol = "output" } });
388             try partitioned.addPartition(.{ .id = .{ .value = 1 }, .name = "caller", .pipeline = .choir, .product = .{ .producer = "composition-fixture", .source = "1", .stage = "allocation.input", .variant = "default" }, .effect = .none, .provenance = .{ .path = "allocation", .symbol = "caller" } });
389             try partitioned.addPartition(.{ .id = .{ .value = 2 }, .name = "callee", .pipeline = .accy, .inputs = &.{.{ .value = 1 }}, .outputs = &.{.{ .value = 2 }}, .product = .{ .producer = "composition-fixture", .source = "2", .stage = "allocation.input", .variant = "default" }, .effect = .none, .provenance = .{ .path = "allocation", .symbol = "callee" } });
390             try partitioned.addCallSite(.{ .id = .{ .value = 1 }, .name = "call", .caller = .{ .value = 1 }, .callee = .{ .value = 2 }, .inputs = &.{.{ .value = 1 }}, .outputs = &.{.{ .value = 2 }}, .provenance = .{ .path = "allocation", .symbol = "call" } });
391 
392             var host_artifact = try choir.backends.artifact.objectFileArtifact(allocator, .{ .architecture = .x86_64 }, .{}, "caller", &.{1}, &.{.{ .offset = 0, .symbol = abi.invoke_symbol, .kind = .call }});
393             defer host_artifact.deinit();
394             var callee_artifact = try choir.backends.artifact.objectFileArtifact(allocator, .{ .architecture = .x86_64 }, .{}, "callee", &.{2}, &.{});
395             defer callee_artifact.deinit();
396 
397             var module = try CompositionModule.init(allocator, &partitioned);
398             defer module.deinit();
399             try module.addFragment(.{ .id = .{ .value = 1 }, .partition = .{ .value = 1 }, .pipeline = .choir, .pipeline_input = try records.key("allocation.input", "1", "1"), .artifacts = &.{host_artifact}, .exports = &.{.{ .name = "caller", .symbol = "caller", .abi_version = abi.version }}, .imports = &.{.{ .name = "invoke", .symbol = abi.invoke_symbol, .abi_version = abi.version, .kind = .runtime }}, .provenance = .{ .path = "allocation", .symbol = "caller" } });
400             try module.addFragment(.{ .id = .{ .value = 2 }, .partition = .{ .value = 2 }, .pipeline = .accy, .pipeline_input = try records.key("allocation.input", "2", "2"), .artifacts = &.{callee_artifact}, .exports = &.{.{ .name = "callee", .symbol = "callee", .abi_version = abi.version }}, .provenance = .{ .path = "allocation", .symbol = "callee" } });
401             try module.addVariant(.{
402                 .id = .{ .value = 1 },
403                 .choice = .{
404                     .target = product.productRef("composition-fixture", "1", "allocation.target", "default"),
405                     .policy = product.productRef("composition-fixture", "1", "allocation.policy", "default"),
406                 },
407                 .fragments = &.{ .{ .value = 1 }, .{ .value = 2 } },
408                 .call_sites = &.{.{ .id = .{ .value = 1 }, .name = "call", .caller = .{ .value = 1 }, .caller_export = "caller", .runtime_import = "invoke", .callee = .{ .value = 2 }, .callee_export = "callee", .inputs = &.{.{ .value = 1 }}, .outputs = &.{.{ .value = 2 }}, .abi_version = abi.version, .provenance = .{ .path = "allocation", .symbol = "call" } }},
409             });
410             try module.verify();
411         }
412     };
413 
414     try std.testing.checkAllAllocationFailures(std.testing.allocator, Harness.run, .{});
415 }