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(&.{&region});
 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 }