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 }