lib/choir/src/product/operation.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const simd = @import("simd");
3 const alloc_arena = @import("alloc_arena");
4 const ir = @import("../core/root.zig");
5 const entity = @import("root.zig").entity;
6 const revision = @import("root.zig").revision;
7 const recipe = @import("root.zig").recipe;
8 const compiler = @import("root.zig").compiler;
9 const bytecode = @import("../bytecode/root.zig");
10 const alloc_fixed = @import("alloc_fixed");
11
12 const Bytes = simd.ScalableTag(u8);
13
14 pub const image_version: u32 = 1;
15 pub const schema_identity = revision.record.Version{
16 .name = "choir-operation-schema",
17 .version = 1,
18 };
19
20 pub const Configuration = struct {
21 context: ir.Context.Limits,
22 register: *const fn (*ir.Context) anyerror!void,
23 registration: revision.record.Version,
24 codec: bytecode.qualification.Limits,
25 image: bytecode.image.Limits,
26 roots: u32,
27 gate_scratch: u32,
28 verify: ir.VerifyOptions,
29 };
30
31 pub const Source = struct {
32 operation: *ir.Operation,
33 resources: []const bytecode.Resource = &.{},
34 };
35
36 /// The trusted composition entry fixes the complete IR gate before freezing its store.
37 pub fn registerKind(
38 allocator: std.mem.Allocator,
39 store: *revision.Store,
40 comptime configuration: Configuration,
41 identity: revision.record.Version,
42 gates: []const revision.store.Gate,
43 ) !*const revision.Kind {
44 comptime classifyConfiguration();
45 _ = try compiler.manifest();
46 try requireGenericVerification(configuration.verify);
47 var writer = revision.record.Writer.init(allocator);
48 defer writer.deinit();
49 try revision.record.writeValue(&writer, .{
50 .image_version = image_version,
51 .bytecode_version = bytecode.format_version,
52 .context = configuration.context,
53 .registration = configuration.registration,
54 .codec = configuration.codec,
55 .image = configuration.image,
56 .roots = configuration.roots,
57 .gate_scratch = configuration.gate_scratch,
58 .verify = configuration.verify,
59 });
60 return store.register(.{
61 .identity = identity,
62 .schema = .{
63 .identity = schema_identity,
64 .definition = writer.bytes.items,
65 .scratch_bytes = configuration.gate_scratch,
66 .run = struct {
67 fn run(
68 input: revision.store.GateInput,
69 scratch: []u8,
70 ) !revision.record.EntityCounts {
71 var fixed = alloc_fixed.FixedBuffer.init(scratch);
72 const inputs = try revision.record.decodeInputs(input.exact.inputs);
73 try requireCompilerManifest(input.compiler_manifest);
74 return verifyImage(
75 fixed.allocator(),
76 input.exact.image,
77 try recipe.decode(inputs.policy),
78 configuration,
79 ) catch |err| return if (err == error.OutOfMemory)
80 error.WorkExhausted
81 else
82 err;
83 }
84 }.run,
85 },
86 .gates = gates,
87 });
88 }
89
90 fn boundedFailure(work: *revision.AccountingV1, err: anyerror) anyerror {
91 if (err == error.OutOfMemory) {
92 work.fail(.exhausted);
93 return error.WorkExhausted;
94 }
95 return err;
96 }
97
98 fn requireCompilerManifest(bytes: []const u8) !void {
99 if (!simd.equal(Bytes, bytes, try compiler.manifest())) {
100 return error.UnrecordedCompilerBuild;
101 }
102 }
103
104 fn classifyConfiguration() void {
105 const fields = revision.record.requireFields;
106 fields(Configuration, &.{
107 "context", "register", "registration", "codec", "image", "roots", "gate_scratch", "verify",
108 });
109 fields(ir.Context.Limits, &.{
110 "maximum_alignment", "configuration", "types", "attributes", "operations", "diagnostics",
111 "transient_bytes",
112 });
113 fields(ir.Context.Limits.Configuration, &.{
114 "table_bytes", "name_bytes", "interface_bytes", "transaction_bytes",
115 });
116 fields(ir.Context.Limits.Types, &.{ "table_bytes", "key_bytes", "payload_bytes" });
117 fields(ir.Context.Limits.Attributes, &.{ "table_bytes", "payload_bytes" });
118 fields(ir.Context.Limits.Operations, &.{ "storage_bytes", "nested_bytes" });
119 fields(ir.Context.Limits.Diagnostics, &.{ "handler_bytes", "payload_bytes" });
120 fields(bytecode.qualification.Limits, &.{ "operations", "entities", "fields", "depth" });
121 fields(bytecode.image.Limits, &.{ "bytes", "entities", "depth" });
122 fields(ir.VerifyOptions, &.{
123 "check_terminators", "require_terminators", "recursive", "check_use_def",
124 "check_local_dominance", "check_cfg", "max_depth",
125 });
126 }
127
128 fn requireGenericVerification(options: ir.VerifyOptions) !void {
129 if (!options.recursive or options.max_depth != 0 or !options.check_use_def or
130 !options.check_local_dominance or !options.check_cfg or !options.check_terminators)
131 {
132 return error.WeakenedOperationSchema;
133 }
134 }
135
136 /// Exclusive job capture copies only qualified bytecode; no source pointer is retained.
137 pub fn capture(
138 builder: *revision.Builder,
139 sources: []const Source,
140 stage_record: []const u8,
141 observations: []const revision.record.Fact,
142 comptime configuration: Configuration,
143 ) !void {
144 const work = builder.accounting();
145 errdefer work.fail(.rejected);
146 if (sources.len == 0 or sources.len > configuration.roots) return error.EmptyOperationProduct;
147 try requireCompilerManifest(builder.compilerManifest());
148 const recorded = try recipe.decode(builder.inputs().policy);
149 for (sources) |source| try recorded.requireContext(source.operation.getContext());
150 try verifySources(sources, work, configuration);
151 const token = try work.begin(.decode, .{
152 .identity = .{ .name = "choir-qualified-bytecode", .version = 1 },
153 .work = .{
154 .input_bytes = configuration.image.bytes,
155 .output_bytes = configuration.image.bytes,
156 .structural_visits = configuration.image.entities,
157 .allocation_capacity = configuration.gate_scratch,
158 },
159 .workspace = configuration.gate_scratch,
160 });
161 var open = true;
162 errdefer if (open) {
163 work.finish(token, .rejected, .{}) catch {};
164 };
165 const scratch = try builder.acquireWorkspace(.scratch);
166 defer scratch.release();
167 var fixed = alloc_fixed.FixedBuffer.init(scratch.bytes());
168 const bytes = captureSources(
169 fixed.allocator(),
170 sources,
171 stage_record,
172 configuration,
173 ) catch |err|
174 return boundedFailure(work, err);
175 defer fixed.allocator().free(bytes);
176 try work.finish(token, .success, .{ .work = .{ .output_bytes = bytes.len } });
177 open = false;
178 try builder.capture(bytes, observations);
179 }
180
181 fn verifySources(
182 sources: []const Source,
183 work: *revision.AccountingV1,
184 comptime configuration: Configuration,
185 ) !void {
186 const token = try work.begin(.verification, .{
187 .identity = .{ .name = "choir-source-structure", .version = 1 },
188 .work = .{ .structural_visits = configuration.image.entities },
189 .workspace = configuration.gate_scratch,
190 });
191 errdefer work.finish(token, .rejected, .{}) catch {};
192 for (sources, 0..) |source, index| {
193 for (sources[0..index]) |other| {
194 if (source.operation == other.operation or
195 source.operation.isAncestor(other.operation) or
196 other.operation.isAncestor(source.operation))
197 {
198 return error.OverlappingOperationRoots;
199 }
200 }
201 try ir.verifyOperation(source.operation, configuration.verify);
202 }
203 try work.finish(token, .success, .{});
204 }
205
206 fn captureSources(
207 allocator: std.mem.Allocator,
208 sources: []const Source,
209 stage_record: []const u8,
210 comptime configuration: Configuration,
211 ) ![]u8 {
212 var writer = revision.record.Writer.init(allocator);
213 defer writer.deinit();
214 try writer.writeInt(u32, image_version);
215 try writer.writeCount(sources.len);
216 for (sources) |source| {
217 var context = try ir.Context.init(allocator, configuration.context);
218 defer context.deinit(allocator);
219 context.arithmetic_policy = source.operation.getContext().arithmetic_policy;
220 try configuration.register(&context);
221 const bytes = try bytecode.qualification.encode(
222 allocator,
223 source.operation,
224 source.resources,
225 &context,
226 configuration.codec,
227 );
228 defer allocator.free(bytes);
229 if (bytes.len > configuration.image.bytes) return error.RecordLimit;
230 try writer.writeBlob(bytes);
231 }
232 try writer.writeBlob(stage_record);
233 if (writer.bytes.items.len > configuration.image.bytes) return error.RecordLimit;
234 return writer.finish();
235 }
236
237 fn verifyImage(
238 allocator: std.mem.Allocator,
239 bytes: []const u8,
240 recorded: recipe.Recipe,
241 comptime configuration: Configuration,
242 ) !revision.record.EntityCounts {
243 var reader = try revision.record.Reader.init(bytes);
244 if (try reader.readInt(u32) != image_version) return error.UnknownSchema;
245 const roots = try reader.readCount();
246 if (roots == 0 or roots > configuration.roots) return error.EmptyOperationProduct;
247 var counts: revision.record.EntityCounts = @splat(0);
248 counts[@backingInt(revision.record.Namespace.root)] = @intCast(roots);
249 for (0..roots) |_| {
250 const encoded = try reader.readBlob();
251 const index = try bytecode.image.Index.create(allocator, encoded, configuration.image);
252 defer index.destroy();
253 try addCounts(&counts, index.view());
254 var context = try ir.Context.init(allocator, configuration.context);
255 defer context.deinit(allocator);
256 recorded.restore(&context);
257 try configuration.register(&context);
258 var decoded = try bytecode.decodeModule(allocator, &context, encoded);
259 defer decoded.deinit();
260 try ir.verifyOperation(decoded.module, configuration.verify);
261 }
262 _ = try reader.readBlob();
263 if (!reader.atEnd()) return error.InvalidRecord;
264 return counts;
265 }
266
267 fn addCounts(counts: *revision.record.EntityCounts, view: bytecode.image.View) !void {
268 inline for (.{ "operation", "region", "block", "value", "attribute" }, .{
269 view.operations.len,
270 view.regions.len,
271 view.blocks.len,
272 view.values.len,
273 view.attributes.len,
274 }) |name, count| {
275 const namespace = @field(revision.record.Namespace, name);
276 const destination = &counts[@backingInt(namespace)];
277 destination.* = std.math.add(u32, destination.*, @intCast(count)) catch {
278 return error.EntityOverflow;
279 };
280 }
281 }
282
283 pub const Product = struct {
284 revision: *const revision.Revision,
285
286 pub fn seal(builder: *revision.Builder, kind: *const revision.Kind) !Product {
287 const published = try builder.seal(kind);
288 return .{ .revision = published };
289 }
290
291 pub fn retain(self: Product) !Product {
292 return .{ .revision = try self.revision.retain() };
293 }
294
295 pub fn release(self: Product) void {
296 self.revision.release();
297 }
298
299 pub fn eql(self: Product, other: Product) bool {
300 return self.revision.eql(other.revision);
301 }
302
303 pub fn open(
304 self: Product,
305 allocator: std.mem.Allocator,
306 limits: bytecode.image.Limits,
307 ) !*entity.Image {
308 try self.revision.requireGates(&.{schema_identity});
309 return entity.Image.create(allocator, self.revision, limits, image_version);
310 }
311
312 pub fn successor(
313 self: Product,
314 builder: *revision.Builder,
315 comptime configuration: Configuration,
316 ) !*Job {
317 const work = builder.accounting();
318 errdefer work.fail(.rejected);
319 try builder.requireDependency("source", self.revision);
320 try self.revision.requireGates(&.{schema_identity});
321 const token = try work.begin(.decode, .{
322 .identity = .{ .name = "choir-successor-decode", .version = 1 },
323 .work = .{
324 .input_bytes = self.revision.view().exact.image.len,
325 .structural_visits = configuration.image.entities,
326 .allocation_capacity = configuration.gate_scratch,
327 },
328 .workspace = configuration.gate_scratch,
329 });
330 errdefer work.finish(token, .rejected, .{}) catch {};
331 const lease = try builder.acquireWorkspace(.producer);
332 const job = JobData.create(lease, self.revision, configuration) catch |err| {
333 lease.release();
334 return boundedFailure(work, err);
335 };
336 errdefer jobData(job).destroy();
337 try work.finish(token, .success, .{ .work = .{
338 .input_bytes = self.revision.view().exact.image.len,
339 } });
340 return job;
341 }
342 };
343
344 /// A single mutable writer decoded from a retained image into an independent Context.
345 /// Analyses belong to the new job; no predecessor cache or live IR crosses this boundary.
346 pub const Job = opaque {
347 pub fn destroy(self: *Job) void {
348 jobData(self).destroy();
349 }
350
351 /// All mutable job allocations consume its pre-reserved producer region.
352 pub fn allocator(self: *Job) std.mem.Allocator {
353 std.debug.assert(!jobData(self).capturing);
354 return jobData(self).allocator;
355 }
356
357 /// Borrowed until job destruction; any refused producer allocation is terminal.
358 pub fn storageExhaustion(self: *Job) *const bool {
359 return &jobData(self).fixed.exhausted;
360 }
361
362 /// Physical producer capacity reserved before decoding or executing the successor.
363 pub fn storageCapacity(self: *Job) usize {
364 return jobData(self).lease.bytes().len;
365 }
366
367 /// Refusal in either the outer region or a Context segment exhausts this job.
368 pub fn exhausted(self: *Job) bool {
369 const state = jobData(self);
370 return state.fixed.exhausted or state.context.exhaustedSegment() != null;
371 }
372
373 pub fn context(self: *Job) *ir.Context {
374 std.debug.assert(!jobData(self).capturing);
375 return &jobData(self).context;
376 }
377
378 pub fn root(self: *Job, ordinal: u32) ?*ir.Operation {
379 const state = jobData(self);
380 std.debug.assert(!state.capturing);
381 if (ordinal >= state.roots.len) return null;
382 return state.roots[ordinal].module;
383 }
384
385 pub fn parent(self: *const Job) *const revision.Revision {
386 const state: *const JobData = @ptrCast(@alignCast(self));
387 return state.parent;
388 }
389
390 pub fn capture(
391 self: *Job,
392 builder: *revision.Builder,
393 stage_record: []const u8,
394 observations: []const revision.record.Fact,
395 comptime configuration: Configuration,
396 ) !void {
397 const state = jobData(self);
398 const work = builder.accounting();
399 errdefer work.fail(.rejected);
400 if (!builder.ownsWorkspace(state.lease)) return error.ForeignJob;
401 if (self.exhausted()) {
402 work.fail(.exhausted);
403 return error.WorkExhausted;
404 }
405 std.debug.assert(!state.capturing);
406 state.capturing = true;
407 defer state.capturing = false;
408 const sources = try state.sourceDescriptors(work);
409 defer state.allocator.free(sources);
410 try @import("root.zig").operation.capture(
411 builder,
412 sources,
413 stage_record,
414 observations,
415 configuration,
416 );
417 }
418 };
419
420 const JobData = struct {
421 allocator: std.mem.Allocator,
422 fixed: alloc_fixed.Tracked,
423 lease: revision.store.WorkspaceLease,
424 parent: *const revision.Revision,
425 context: ir.Context,
426 roots: []bytecode.DecodedModule,
427 capturing: bool = false,
428
429 fn sourceDescriptors(self: *JobData, work: *revision.AccountingV1) ![]Source {
430 const bytes = std.math.mul(usize, self.roots.len, @sizeOf(Source)) catch {
431 return error.RecordOverflow;
432 };
433 const token = try work.begin(.input, .{
434 .identity = .{ .name = "choir-root-bindings", .version = 1 },
435 .work = .{ .allocation_capacity = bytes },
436 .workspace = bytes,
437 });
438 errdefer work.finish(token, .rejected, .{}) catch {};
439 const sources = try self.allocator.alloc(Source, self.roots.len);
440 errdefer self.allocator.free(sources);
441 for (sources, self.roots) |*source, decoded| source.* = .{
442 .operation = decoded.module,
443 .resources = decoded.resources,
444 };
445 try work.finish(token, .success, .{ .work = .{ .allocation_capacity = bytes } });
446 return sources;
447 }
448
449 fn create(
450 lease: revision.store.WorkspaceLease,
451 parent: *const revision.Revision,
452 comptime configuration: Configuration,
453 ) !*Job {
454 const retained = try parent.retain();
455 errdefer retained.release();
456 var initial = alloc_fixed.Tracked.init(lease.bytes());
457 const self = try initial.allocator().create(JobData);
458 self.* = .{
459 .allocator = undefined,
460 .fixed = initial,
461 .lease = lease,
462 .parent = retained,
463 .roots = &.{},
464 .context = undefined,
465 };
466 self.allocator = self.fixed.allocator();
467 const allocator = self.allocator;
468 self.context = try ir.Context.init(allocator, configuration.context);
469 errdefer self.context.deinit(allocator);
470 (try recipe.decode(parent.inputs().policy)).restore(&self.context);
471 try configuration.register(&self.context);
472 var reader = try revision.record.Reader.init(parent.view().exact.image);
473 if (try reader.readInt(u32) != image_version) return error.UnknownSchema;
474 const count = try reader.readCount();
475 if (count == 0 or count > configuration.roots) return error.ImageLimit;
476 self.roots = try allocator.alloc(bytecode.DecodedModule, count);
477 errdefer allocator.free(self.roots);
478 var initialized: usize = 0;
479 errdefer for (self.roots[0..initialized]) |*decoded| decoded.deinit();
480 for (self.roots) |*decoded| {
481 decoded.* = try bytecode.decodeModule(allocator, &self.context, try reader.readBlob());
482 initialized += 1;
483 }
484 _ = try reader.readBlob();
485 if (!reader.atEnd()) return error.InvalidRecord;
486 return @ptrCast(self);
487 }
488
489 fn destroy(self: *JobData) void {
490 std.debug.assert(!self.capturing);
491 for (self.roots) |*decoded| decoded.deinit();
492 self.context.deinit(self.allocator);
493 self.allocator.free(self.roots);
494 self.parent.release();
495 const lease = self.lease;
496 lease.release();
497 }
498 };
499
500 fn jobData(job: *Job) *JobData {
501 return @ptrCast(@alignCast(job));
502 }
503
504 fn registerPublicationTest(context: *ir.Context) !void {
505 try context.allowUnregistered();
506 }
507
508 const publication_test_configuration = Configuration{
509 .context = ir.Context.Limits.testing,
510 .register = registerPublicationTest,
511 .registration = .{ .name = "unregistered-test-context", .version = 1 },
512 .codec = .{ .operations = 100, .entities = 1000, .fields = 1000, .depth = 32 },
513 .image = .{ .bytes = 65536, .entities = 1024, .depth = 32 },
514 .roots = 4,
515 .gate_scratch = 64 * 1024 * 1024,
516 .verify = ir.verify.default_options,
517 };
518
519 const publication_test_limits = revision.store.Limits{
520 .revisions = 8,
521 .kinds = 4,
522 .builders = 4,
523 .compiler_manifests = 2,
524 .record_bytes = 32 * 1024 * 1024,
525 .gate_scratch_bytes = publication_test_configuration.gate_scratch,
526 .candidate_count = 8,
527 .screening_bytes = 32 * 1024 * 1024,
528 };
529
530 fn publishTestTree(
531 allocator: std.mem.Allocator,
532 store: *revision.Store,
533 kind: *const revision.Kind,
534 threshold: u8,
535 arithmetic: recipe.ArithmeticPolicy,
536 ) !Product {
537 var arena = alloc_arena.Arena.init(allocator);
538 defer arena.deinit();
539 var context = try ir.Context.init(arena.allocator(), ir.Context.Limits.testing);
540 defer context.deinit(arena.allocator());
541 context.arithmetic_policy = arithmetic;
542 try registerPublicationTest(&context);
543 const tree = try makeTestTree(&context, "product.child");
544 try tree.child.setAttr("threshold", try context.getI64Attr(threshold));
545 const options = [_]u8{threshold};
546 const policy = try recipe.encode(allocator, .{ .arithmetic = arithmetic, .policy = "strict" });
547 defer allocator.free(policy);
548 const builder = try store.begin(.{
549 .kind = kind,
550 .address = .{ .producer = "choir", .source = "test", .stage = "operation", .variant = "" },
551 .inputs = .{
552 .compiler_manifest = try compiler.manifest(),
553 .versions = &.{schema_identity},
554 .pipeline = &.{},
555 .options = &options,
556 .policy = policy,
557 },
558 }, .{
559 .allowance = revision.WorkVector.uniform(1024 * 1024 * 1024),
560 .workspace = 128 * 1024 * 1024,
561 .events = 32,
562 });
563 var builder_open = true;
564 errdefer if (builder_open) {
565 if (builder.abort(.rejected)) |failure| {
566 var owned = failure;
567 owned.deinit();
568 }
569 };
570 try capture(
571 builder,
572 &.{.{ .operation = tree.root }},
573 "stage",
574 &.{},
575 publication_test_configuration,
576 );
577 const published = try Product.seal(builder, kind);
578 builder_open = false;
579 errdefer published.release();
580 try tree.child.setAttr("threshold", try context.getI64Attr(255));
581 return published;
582 }
583
584 test "sealed operation products retain exact immutable entities across Context lifetimes" {
585 const allocator = std.testing.allocator;
586 const store = try revision.Store.create(allocator, publication_test_limits);
587 defer store.release();
588 const kind = try registerKind(allocator, store, publication_test_configuration, .{
589 .name = "choir.operation",
590 .version = 1,
591 }, &.{});
592 store.freeze();
593 const first = try publishTestTree(allocator, store, kind, 11, .{});
594 defer first.release();
595 const second = try publishTestTree(allocator, store, kind, 11, .{});
596 defer second.release();
597 const changed = try publishTestTree(allocator, store, kind, 12, .{});
598 defer changed.release();
599 try std.testing.expect(first.eql(second));
600 try std.testing.expect(!first.eql(changed));
601 try std.testing.expectEqualSlices(
602 u8,
603 first.revision.view().exact.address,
604 changed.revision.view().exact.address,
605 );
606 const reference = try first.revision.entity(.operation, 1);
607 defer reference.release();
608 const view = try second.open(allocator, publication_test_configuration.image);
609 defer view.destroy();
610 try std.testing.expectEqualStrings("product.child", (try view.operation(reference)).value.name);
611 try std.testing.expectEqualStrings("stage", view.stage());
612 const successor_view = try changed.open(allocator, publication_test_configuration.image);
613 defer successor_view.destroy();
614 try std.testing.expectError(error.StaleEntity, successor_view.operation(reference));
615 }
616
617 fn beginTestSuccessor(
618 store: *revision.Store,
619 kind: *const revision.Kind,
620 parent: Product,
621 workspace: u64,
622 ) !*revision.Builder {
623 return store.begin(.{
624 .kind = kind,
625 .address = .{ .producer = "choir", .source = "test", .stage = "operation", .variant = "" },
626 .inputs = .{
627 .compiler_manifest = try compiler.manifest(),
628 .versions = &.{schema_identity},
629 .pipeline = &.{},
630 .options = "successor",
631 .policy = parent.revision.inputs().policy,
632 },
633 .parent = parent.revision,
634 .dependencies = &.{.{ .role = "source", .revision = parent.revision }},
635 }, .{
636 .allowance = revision.WorkVector.uniform(1024 * 1024 * 1024),
637 .workspace = workspace,
638 .events = 32,
639 });
640 }
641
642 fn firstChild(root: *ir.Operation) *ir.Operation {
643 var operations = root.getRegion(0).?.getEntryBlock().?.getOperations();
644 return operations.next().?;
645 }
646
647 test "sealed operation successors isolate mutation and discard failed unpublished jobs" {
648 const allocator = std.testing.allocator;
649 const store = try revision.Store.create(allocator, publication_test_limits);
650 defer store.release();
651 const kind = try registerKind(allocator, store, publication_test_configuration, .{
652 .name = "choir.operation",
653 .version = 1,
654 }, &.{});
655 store.freeze();
656 const ancestor = try publishTestTree(allocator, store, kind, 11, .{});
657 defer ancestor.release();
658 const retained_bytes = try allocator.dupe(u8, ancestor.revision.view().semantic_record);
659 defer allocator.free(retained_bytes);
660 const first_builder = try beginTestSuccessor(store, kind, ancestor, 128 * 1024 * 1024);
661 const second_builder = try beginTestSuccessor(store, kind, ancestor, 128 * 1024 * 1024);
662 const first = try ancestor.successor(first_builder, publication_test_configuration);
663 defer first.destroy();
664 const second = try ancestor.successor(
665 second_builder,
666 publication_test_configuration,
667 );
668 defer second.destroy();
669 try std.testing.expect(first.context() != second.context());
670 try std.testing.expect(first.root(0).? != second.root(0).?);
671 const changed = firstChild(first.root(0).?);
672 const untouched = firstChild(second.root(0).?);
673 try changed.setAttr("threshold", try first.context().getI64Attr(12));
674 try std.testing.expectEqual(
675 11,
676 untouched.getAttr("threshold").?.cast(ir.Attribute.IntegerAttr).?.value,
677 );
678 const parent_block = changed.parent_block;
679 changed.parent_block = null;
680 try std.testing.expectError(error.ParentBlockMismatch, first.capture(
681 first_builder,
682 "stage",
683 &.{},
684 publication_test_configuration,
685 ));
686 changed.parent_block = parent_block;
687 var failure = first_builder.abort(.rejected).?;
688 defer failure.deinit();
689 try std.testing.expectEqual(.rejected, failure.work.outcome);
690 try std.testing.expectEqual(1, store.publicationCount());
691 try second.capture(second_builder, "stage", &.{}, publication_test_configuration);
692 const successor = try Product.seal(second_builder, kind);
693 defer successor.release();
694 try std.testing.expectEqualSlices(u8, retained_bytes, ancestor.revision.view().semantic_record);
695 try std.testing.expectEqual(
696 ancestor.revision.view().event.counter,
697 successor.revision.view().event.parent.?.counter,
698 );
699 try std.testing.expectEqualSlices(
700 u8,
701 ancestor.revision.view().exact.image,
702 successor.revision.view().exact.image,
703 );
704 try std.testing.expect(!ancestor.eql(successor));
705 }
706
707 test "sealed operation registration refuses weakened generic verification" {
708 const allocator = std.testing.allocator;
709 const store = try revision.Store.create(allocator, publication_test_limits);
710 defer store.release();
711 const weak = comptime block: {
712 var configuration = publication_test_configuration;
713 configuration.verify.recursive = false;
714 break :block configuration;
715 };
716 try std.testing.expectError(error.WeakenedOperationSchema, registerKind(
717 allocator,
718 store,
719 weak,
720 .{ .name = "weak", .version = 1 },
721 &.{},
722 ));
723 }
724
725 fn allocationPublication(allocator: std.mem.Allocator) !void {
726 const store = try revision.Store.create(allocator, publication_test_limits);
727 defer store.release();
728 const kind = try registerKind(allocator, store, publication_test_configuration, .{
729 .name = "choir.operation",
730 .version = 1,
731 }, &.{});
732 store.freeze();
733 const published = try publishTestTree(allocator, store, kind, 11, .{});
734 defer published.release();
735 const view = try published.open(allocator, publication_test_configuration.image);
736 defer view.destroy();
737 try std.testing.expectEqualStrings("product.root", view.root(0).?.operations[0].name);
738 }
739
740 test "sealed operation publication and retained indexing clean allocation failures" {
741 var backing = std.testing.FailingAllocator.init(std.testing.allocator, .{
742 .resize_fail_index = 0,
743 });
744 try std.testing.checkAllAllocationFailures(backing.allocator(), allocationPublication, .{});
745 }
746
747 const TestTree = struct {
748 root: *ir.Operation,
749 region: *ir.Region,
750 block: *ir.Block,
751 child: *ir.Operation,
752 argument: *ir.Value,
753 result: *ir.Value,
754 attribute: ir.Attribute,
755 };
756
757 fn makeTestTree(context: *ir.Context, name: []const u8) !TestTree {
758 const test_dialect = @import("../dialects/fixture/root.zig");
759 const location = ir.Location.getUnknown();
760 const integer_type = try test_dialect.TestDialect.getI64Type(context);
761 var region = ir.context.initRegion(context);
762 defer region.deinit();
763 const block = try region.addBlock();
764 const argument = try block.addArgument(integer_type, location);
765 var child_state = ir.Operation.State.init(name, location);
766 child_state.addOperands(&.{argument});
767 child_state.addTypes(&.{integer_type});
768 const child = try context.createOperation(child_state);
769 try block.addOperation(child);
770 var root_state = ir.Operation.State.init("product.root", location);
771 root_state.addRegionBodies(&.{®ion});
772 const root = try context.createOperation(root_state);
773 const leaf = try context.getStringAttr("entity");
774 const attribute = try context.getArrayAttr(&.{leaf});
775 try child.setAttr("payload", attribute);
776 return .{
777 .root = root,
778 .region = &root.regions.items[0],
779 .block = root.getRegion(0).?.getEntryBlock().?,
780 .child = child,
781 .argument = argument,
782 .result = child.getResult(0).?,
783 .attribute = leaf,
784 };
785 }
786
787 test "sealed operation recipes restore arithmetic policy and refuse unrecorded changes" {
788 const allocator = std.testing.allocator;
789 const store = try revision.Store.create(allocator, publication_test_limits);
790 defer store.release();
791 const kind = try registerKind(allocator, store, publication_test_configuration, .{
792 .name = "choir.operation",
793 .version = 1,
794 }, &.{});
795 store.freeze();
796 const ordinary = try publishTestTree(allocator, store, kind, 11, .{});
797 defer ordinary.release();
798 const policy = recipe.ArithmeticPolicy{
799 .exceptions_masked = false,
800 .default_rounding = false,
801 .environment_observable = true,
802 };
803 const changed = try publishTestTree(allocator, store, kind, 11, policy);
804 defer changed.release();
805 try std.testing.expect(!ordinary.eql(changed));
806 try std.testing.expectEqualSlices(
807 u8,
808 ordinary.revision.view().exact.image,
809 changed.revision.view().exact.image,
810 );
811 const builder = try beginTestSuccessor(store, kind, changed, 128 * 1024 * 1024);
812 defer if (builder.abort(.rejected)) |failure| {
813 var owned = failure;
814 owned.deinit();
815 };
816 const job = try changed.successor(builder, publication_test_configuration);
817 defer job.destroy();
818 try std.testing.expectEqualDeep(policy, job.context().arithmetic_policy);
819 job.context().arithmetic_policy = .{};
820 try std.testing.expectError(error.UnrecordedArithmeticPolicy, job.capture(
821 builder,
822 "stage",
823 &.{},
824 publication_test_configuration,
825 ));
826 try std.testing.expectEqual(.rejected, builder.accounting().view().outcome);
827 const retained = try changed.open(allocator, publication_test_configuration.image);
828 defer retained.destroy();
829 try std.testing.expectEqualStrings("stage", retained.stage());
830 }
831
832 test "unrecorded compiler builds refuse operation publication registration" {
833 if (compiler.refusal == null) return error.SkipZigTest;
834 const allocator = std.testing.allocator;
835 const store = try revision.Store.create(allocator, publication_test_limits);
836 defer store.release();
837 try std.testing.expectError(error.UnrecordedCompilerBuild, registerKind(
838 allocator,
839 store,
840 publication_test_configuration,
841 .{ .name = "choir.operation", .version = 1 },
842 &.{},
843 ));
844 try std.testing.expectEqual(0, store.publicationCount());
845 }
846
847 test "sealed operation schema rejects absent and changed compiler manifests on copied records" {
848 const allocator = std.testing.allocator;
849 const store = try revision.Store.create(allocator, publication_test_limits);
850 defer store.release();
851 const kind = try registerKind(allocator, store, publication_test_configuration, .{
852 .name = "choir.operation",
853 .version = 1,
854 }, &.{});
855 store.freeze();
856 const published = try publishTestTree(allocator, store, kind, 11, .{});
857 defer published.release();
858 const changed = try allocator.dupe(u8, try compiler.manifest());
859 defer allocator.free(changed);
860 changed[changed.len - 1] ^= 1;
861 for ([_][]const u8{ "", changed }) |manifest| {
862 const request = revision.store.Request{
863 .kind = kind,
864 .address = published.revision.address(),
865 .inputs = .{
866 .compiler_manifest = manifest,
867 .versions = &.{schema_identity},
868 .pipeline = &.{},
869 .options = "forged compiler record",
870 .policy = published.revision.inputs().policy,
871 },
872 };
873 const limits = revision.receipt.Limits{
874 .allowance = revision.WorkVector.uniform(1024 * 1024 * 1024),
875 .workspace = 128 * 1024 * 1024,
876 .events = 32,
877 };
878 if (manifest.len == 0) {
879 try std.testing.expectError(
880 error.MissingCompilerManifest,
881 store.begin(request, limits),
882 );
883 continue;
884 }
885 const builder = try store.begin(request, limits);
886 var builder_open = true;
887 defer if (builder_open) {
888 if (builder.abort(.rejected)) |failure| {
889 var owned = failure;
890 owned.deinit();
891 }
892 };
893 const work = builder.accounting();
894 try builder.capture(published.revision.view().exact.image, &.{});
895 const result = Product.seal(builder, kind);
896 if (result) |unexpected| {
897 builder_open = false;
898 unexpected.release();
899 } else |_| {}
900 try std.testing.expectError(error.UnrecordedCompilerBuild, result);
901 try std.testing.expectEqual(.rejected, work.view().outcome);
902 try std.testing.expectEqual(1, store.publicationCount());
903 }
904 }
905
906 test "sealed operation job uses reserved storage and remains writable after publication" {
907 var observed = std.testing.FailingAllocator.init(std.testing.allocator, .{});
908 const allocator = observed.allocator();
909 const store = try revision.Store.create(allocator, publication_test_limits);
910 defer store.release();
911 const kind = try registerKind(allocator, store, publication_test_configuration, .{
912 .name = "choir.operation",
913 .version = 1,
914 }, &.{});
915 store.freeze();
916 const ancestor = try publishTestTree(allocator, store, kind, 11, .{});
917 defer ancestor.release();
918 const builder = try beginTestSuccessor(store, kind, ancestor, 128 * 1024 * 1024);
919 var open = true;
920 defer if (open) {
921 if (builder.abort(.rejected)) |failure| {
922 var owned = failure;
923 owned.deinit();
924 }
925 };
926 const before = observed.allocated_bytes;
927 const job = try ancestor.successor(builder, publication_test_configuration);
928 defer job.destroy();
929 try std.testing.expectEqual(before, observed.allocated_bytes);
930 const child = firstChild(job.root(0).?);
931 try child.setAttr("threshold", try job.context().getI64Attr(12));
932 try job.capture(builder, "stage", &.{}, publication_test_configuration);
933 const published = try Product.seal(builder, kind);
934 open = false;
935 defer published.release();
936 const image = try allocator.dupe(u8, published.revision.view().exact.image);
937 defer allocator.free(image);
938 try child.setAttr("threshold", try job.context().getI64Attr(13));
939 try std.testing.expectEqual(
940 13,
941 child.getAttr("threshold").?.cast(ir.Attribute.IntegerAttr).?.value,
942 );
943 try std.testing.expectEqualSlices(u8, image, published.revision.view().exact.image);
944 try std.testing.expect(!ancestor.eql(published));
945 }
946
947 test "sealed operation reserved producer exhaustion retains an exhausted phase and no output" {
948 const allocator = std.testing.allocator;
949 const store = try revision.Store.create(allocator, publication_test_limits);
950 defer store.release();
951 const kind = try registerKind(allocator, store, publication_test_configuration, .{
952 .name = "choir.operation",
953 .version = 1,
954 }, &.{});
955 store.freeze();
956 const ancestor = try publishTestTree(allocator, store, kind, 11, .{});
957 defer ancestor.release();
958 const builder = try beginTestSuccessor(
959 store,
960 kind,
961 ancestor,
962 publication_test_configuration.gate_scratch + 1024,
963 );
964 const result = ancestor.successor(builder, publication_test_configuration);
965 if (result) |job| job.destroy() else |_| {}
966 var failure = builder.abort(.rejected).?;
967 defer failure.deinit();
968 try std.testing.expectError(error.WorkExhausted, result);
969 try std.testing.expectEqual(.exhausted, failure.work.outcome);
970 try std.testing.expectEqual(
971 .exhausted,
972 failure.work.events[failure.work.events.len - 1].outcome,
973 );
974 try std.testing.expectEqual(1, store.publicationCount());
975 try std.testing.expectEqual(0, failure.work.executed.counters.pass_runs);
976 }
977
978 test "sealed operation exhausted job cannot capture under a fresh allowance" {
979 const allocator = std.testing.allocator;
980 const store = try revision.Store.create(allocator, publication_test_limits);
981 defer store.release();
982 const kind = try registerKind(allocator, store, publication_test_configuration, .{
983 .name = "choir.operation",
984 .version = 1,
985 }, &.{});
986 store.freeze();
987 const ancestor = try publishTestTree(allocator, store, kind, 11, .{});
988 defer ancestor.release();
989 const image = try allocator.dupe(u8, ancestor.revision.view().exact.image);
990 defer allocator.free(image);
991 const capacity = 128 * 1024 * 1024;
992 const original = try beginTestSuccessor(store, kind, ancestor, capacity);
993 var open = true;
994 defer if (open) {
995 if (original.abort(.rejected)) |failure| {
996 var owned = failure;
997 owned.deinit();
998 }
999 };
1000 const job = try ancestor.successor(original, publication_test_configuration);
1001 defer job.destroy();
1002 try firstChild(job.root(0).?).setAttr("threshold", try job.context().getI64Attr(12));
1003 try std.testing.expectError(error.WorkExhausted, original.accounting().begin(.verification, .{
1004 .identity = .{ .name = "test-job-exhaustion", .version = 1 },
1005 .work = .{},
1006 .workspace = capacity + 1,
1007 }));
1008 var original_failure = original.abort(.exhausted).?;
1009 open = false;
1010 defer original_failure.deinit();
1011 const fresh = try beginTestSuccessor(store, kind, ancestor, capacity);
1012 const rebound = job.capture(fresh, "stage", &.{}, publication_test_configuration);
1013 var fresh_failure = fresh.abort(.rejected).?;
1014 defer fresh_failure.deinit();
1015 try std.testing.expectError(error.ForeignJob, rebound);
1016 try std.testing.expectEqual(.exhausted, original_failure.work.outcome);
1017 try std.testing.expectEqual(.rejected, fresh_failure.work.outcome);
1018 try std.testing.expectEqual(1, store.publicationCount());
1019 try std.testing.expectEqualSlices(u8, image, ancestor.revision.view().exact.image);
1020 }
1021
1022 test "sealed operation refuses publication after a caught producer storage exhaustion" {
1023 try checkJobStorageExhaustion(false);
1024 }
1025
1026 test "sealed operation refuses publication after a caught Context storage exhaustion" {
1027 try checkJobStorageExhaustion(true);
1028 }
1029
1030 fn checkJobStorageExhaustion(exhaust_context: bool) !void {
1031 const allocator = std.testing.allocator;
1032 const store = try revision.Store.create(allocator, publication_test_limits);
1033 defer store.release();
1034 const kind = try registerKind(allocator, store, publication_test_configuration, .{
1035 .name = "choir.operation",
1036 .version = 1,
1037 }, &.{});
1038 store.freeze();
1039 const ancestor = try publishTestTree(allocator, store, kind, 11, .{});
1040 defer ancestor.release();
1041 const capacity = 128 * 1024 * 1024;
1042 const builder = try beginTestSuccessor(store, kind, ancestor, capacity);
1043 var open = true;
1044 defer if (open) {
1045 if (builder.abort(.rejected)) |failure| {
1046 var owned = failure;
1047 owned.deinit();
1048 }
1049 };
1050 const job = try ancestor.successor(builder, publication_test_configuration);
1051 defer job.destroy();
1052 if (exhaust_context) {
1053 const bytes = publication_test_configuration.context.attributes.payload_bytes + 1;
1054 const payload = try allocator.alloc(u8, bytes);
1055 defer allocator.free(payload);
1056 @memset(payload, 'x');
1057 try std.testing.expectError(error.OutOfMemory, job.context().getStringAttr(payload));
1058 try std.testing.expect(!job.storageExhaustion().*);
1059 try std.testing.expectEqual(.attribute_payloads, job.context().exhaustedSegment().?);
1060 } else {
1061 try std.testing.expectError(error.OutOfMemory, job.allocator().alloc(u8, capacity + 1));
1062 }
1063 try std.testing.expect(job.exhausted());
1064 const captured = job.capture(builder, "stage", &.{}, publication_test_configuration);
1065 var failure = builder.abort(.rejected).?;
1066 open = false;
1067 defer failure.deinit();
1068 try std.testing.expectError(error.WorkExhausted, captured);
1069 try std.testing.expectEqual(.exhausted, failure.work.outcome);
1070 try std.testing.expectEqual(1, store.publicationCount());
1071 }