lib/accy/src/preparation/publication.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const fixed = @import("alloc_fixed");
  3 const choir = @import("choir");
  4 const publication = @import("../choir/root.zig").publication;
  5 const records = @import("../choir/root.zig").record;
  6 const preparation = @import("root.zig");
  7 const recipe = @import("recipe.zig");
  8 const execution = @import("execution.zig");
  9 const Stage = publication.Stage;
 10 const operation = choir.product.operation;
 11 const revision = choir.product.revision;
 12 const Options = preparation.BackendPreparationRunOptions;
 13 const bounds = choir.passes.pass.work;
 14 const Prepared = @import("product.zig").BackendPreparedModule;
 15 const Semantic = @import("../choir/root.zig").semantic.SemanticModule;
 16 
 17 /// An immutable handle keeps the first stage record, the sealed result of one compile stage. That
 18 /// record is captured from a draft, the caller's mutable semantic module. A caller holds this
 19 /// handle to compile the same program again with no draft as long as that stage record still
 20 /// matches. Only this file creates one, and it copies the target settings it reads from the draft
 21 /// before the draft is freed. `retain` returns another independent handle to the same record, and
 22 /// `deinit` releases this handle.
 23 pub const SemanticSource = opaque {
 24     const Data = struct {
 25         allocator: std.mem.Allocator,
 26         revision: *const revision.Revision,
 27         target_recipe: []u8,
 28     };
 29 
 30     fn data(self: *const SemanticSource) *const Data {
 31         return @ptrCast(@alignCast(self));
 32     }
 33 
 34     fn create(
 35         allocator: std.mem.Allocator,
 36         published: *const revision.Revision,
 37         target_recipe: []const u8,
 38     ) !*SemanticSource {
 39         const retained = try published.retain();
 40         errdefer retained.release();
 41         const bytes = try allocator.dupe(u8, target_recipe);
 42         errdefer allocator.free(bytes);
 43         const owned = try allocator.create(Data);
 44         owned.* = .{ .allocator = allocator, .revision = retained, .target_recipe = bytes };
 45         return @ptrCast(owned);
 46     }
 47 
 48     fn capture(
 49         allocator: std.mem.Allocator,
 50         draft: *Semantic,
 51         published: *const revision.Revision,
 52     ) !*SemanticSource {
 53         try published.requireGates(&.{ operation.schema_identity, Stage.semantic.schema() });
 54         std.debug.assert(std.mem.eql(u8, published.address().stage, Stage.semantic.name()));
 55         const bytes = try recipe.encode(allocator, .kernel, draft.choir_module, .{});
 56         defer allocator.free(bytes);
 57         return create(allocator, published, bytes);
 58     }
 59 
 60     /// Returns the first stage record this source was built from. The record is borrowed until this
 61     /// source is destroyed, and a caller that needs it longer retains it.
 62     pub fn record(self: *const SemanticSource) *const revision.Revision {
 63         return self.data().revision;
 64     }
 65 
 66     pub fn retain(self: *const SemanticSource, allocator: std.mem.Allocator) !*SemanticSource {
 67         return create(allocator, self.record(), self.data().target_recipe);
 68     }
 69 
 70     pub fn deinit(self: *SemanticSource) void {
 71         const owned = self.data();
 72         owned.revision.release();
 73         owned.allocator.free(owned.target_recipe);
 74         owned.allocator.destroy(@constCast(owned));
 75     }
 76 
 77     fn options(
 78         self: *const SemanticSource,
 79         allocator: std.mem.Allocator,
 80         comptime stage: Stage,
 81         run: Options,
 82     ) ![]u8 {
 83         var inherited = try records.codec.decode(
 84             allocator,
 85             recipe.Record(.kernel),
 86             .kernel,
 87             self.data().target_recipe,
 88         );
 89         defer inherited.deinit();
 90         return recipe.encodeWithTarget(allocator, stage, inherited.value.options, run);
 91     }
 92 };
 93 
 94 pub const SemanticInput = union(enum) {
 95     draft: *Semantic,
 96     retained: *const SemanticSource,
 97 
 98     fn arithmetic(self: SemanticInput) !choir.product.recipe.ArithmeticPolicy {
 99         return switch (self) {
100             .draft => |source| source.context().arithmetic_policy,
101             .retained => |source| (try choir.product.recipe.decode(
102                 source.record().inputs().policy,
103             )).arithmetic,
104         };
105     }
106 
107     fn options(
108         self: SemanticInput,
109         allocator: std.mem.Allocator,
110         comptime stage: Stage,
111         run: Options,
112     ) ![]u8 {
113         return switch (self) {
114             .draft => |source| recipe.encode(allocator, stage, source.choir_module, run),
115             .retained => |source| source.options(allocator, stage, run),
116         };
117     }
118 
119     fn retainSource(
120         self: SemanticInput,
121         allocator: std.mem.Allocator,
122         published: *const revision.Revision,
123     ) !*SemanticSource {
124         return switch (self) {
125             .draft => |source| SemanticSource.capture(allocator, source, published),
126             .retained => |source| retained: {
127                 std.debug.assert(source.record().eql(published));
128                 break :retained source.retain(allocator);
129             },
130         };
131     }
132 };
133 
134 pub const PreparationRequest = struct {
135     source: []const u8,
136     variant: []const u8 = "",
137     options: Options = .{},
138     /// Work limits a caller sets on each compile request, bounding how much work the whole compile
139     /// may do: the allowance in these limits is shared by the whole chain of seven stages, and each
140     /// stage's charge is taken off before the next stage starts. The workspace and trace limits
141     /// apply to each stage separately.
142     work: revision.receipt.Limits,
143     record_bytes: u32,
144     candidate: ?*const Prepared = null,
145 };
146 
147 pub const StageExecution = union(enum) {
148     cold: *const revision.Revision,
149     warm: revision.store.Reuse,
150 
151     pub fn record(self: *const StageExecution) *const revision.Revision {
152         return switch (self.*) {
153             .cold => |item| item,
154             .warm => |item| item.revision,
155         };
156     }
157 
158     pub fn work(self: *const StageExecution) revision.WorkReceiptV1 {
159         return switch (self.*) {
160             .cold => |item| item.view().work,
161             .warm => |item| item.work,
162         };
163     }
164 
165     pub fn deinit(self: *StageExecution) void {
166         switch (self.*) {
167             .cold => |item| item.release(),
168             .warm => |*item| item.deinit(),
169         }
170         self.* = undefined;
171     }
172 };
173 
174 /// Report passed empty to each compile and read afterwards to see what ran, what was reused and
175 /// what failed: the report holds, for each finished stage, whether its stage record was reused or
176 /// built new, with the work it charged, and also the time per stage, the count of finished stages,
177 /// the failure receipt and the retained source. The entries for finished stages stay valid whether
178 /// the compile succeeds or a later stage fails. The caller owns the report, separately from the
179 /// compiled result.
180 pub const PreparationReport = struct {
181     stages: [7]StageExecution = undefined,
182     /// Holds the wall-clock time of each of the seven stages, in nanoseconds, read from the
183     /// request's clock, so a caller can see how long each stage took in this request. These times
184     /// are left out of stage records, out of the charged work and out of the reuse check, so they
185     /// never change a result.
186     elapsed_ns: [7]u64 = @splat(0),
187     completed: u8 = 0,
188     failure: ?revision.store.Failure = null,
189     source: ?*SemanticSource = null,
190 
191     pub fn deinit(self: *PreparationReport) void {
192         if (self.source) |source| source.deinit();
193         for (self.stages[0..self.completed]) |*item| item.deinit();
194         if (self.failure) |*failure| failure.deinit();
195         self.* = .{};
196     }
197 
198     pub fn charged(self: *const PreparationReport) !revision.WorkVector {
199         var result: revision.WorkVector = .{};
200         for (self.stages[0..self.completed]) |*item| result = try result.add(item.work().charged);
201         if (self.failure) |failure| result = try result.add(failure.work.charged);
202         return result;
203     }
204 };
205 
206 /// Compiles a program through all seven stages in order, starting from a fresh draft or from a
207 /// retained source: for each stage after the first, the call reuses the record of the candidate, an
208 /// earlier compile result, when the reuse check accepts it, and builds a new record otherwise. A
209 /// retained source must pass the reuse check for the first stage, and when it does not, the call
210 /// returns `error.SemanticModuleRequired` because only a draft can be captured again. `report` must
211 /// be empty when passed in, and on success its source is set and the caller owns the returned
212 /// result. On failure the call records the failing stage's receipt and the stages already finished
213 /// in `report`, returns the error, and leaves the candidate unchanged.
214 pub fn prepare(
215     allocator: std.mem.Allocator,
216     source: SemanticInput,
217     request: PreparationRequest,
218     report: *PreparationReport,
219     comptime configuration: operation.Configuration,
220 ) !*Prepared {
221     std.debug.assert(report.completed == 0);
222     std.debug.assert(report.failure == null);
223     std.debug.assert(report.source == null);
224     const store = try revision.Store.create(allocator, .{
225         .revisions = 7,
226         .kinds = 7,
227         .builders = 1,
228         .compiler_manifests = 2,
229         .record_bytes = request.record_bytes,
230         .gate_scratch_bytes = configuration.gate_scratch,
231         .candidate_count = 7,
232         .screening_bytes = request.record_bytes,
233     });
234     defer store.release();
235     const stages = comptime std.enums.values(Stage);
236     var kinds: [7]*const revision.Kind = undefined;
237     inline for (stages, 0..) |stage, index| {
238         kinds[index] = try registerKind(allocator, store, stage, configuration);
239     }
240     store.freeze();
241     var remaining = request.work;
242     var revisions: [7]*const revision.Revision = undefined;
243     inline for (stages, 0..) |stage, index| {
244         const start = request.options.now();
245         defer report.elapsed_ns[index] = @intCast(@max(0, request.options.now() - start));
246         const parent = if (index == 0) null else revisions[index - 1];
247         const builder = try beginPreparationStage(
248             allocator,
249             store,
250             kinds[index],
251             source,
252             parent,
253             stage,
254             request,
255             remaining,
256         );
257         report.stages[index] = prepareStage(
258             allocator,
259             builder,
260             kinds[index],
261             source,
262             parent,
263             stage,
264             request,
265             configuration,
266         ) catch |err| {
267             report.failure = builder.abort(.rejected);
268             return err;
269         };
270         report.completed += 1;
271         revisions[index] = report.stages[index].record();
272         const charged = report.stages[index].work().charged;
273         inline for (@typeInfo(revision.WorkVector).@"struct".field_names) |field| {
274             std.debug.assert(@field(charged, field) <= @field(remaining.allowance, field));
275             @field(remaining.allowance, field) -= @field(charged, field);
276         }
277     }
278     report.source = try source.retainSource(allocator, revisions[0]);
279     return Prepared.create(allocator, revisions);
280 }
281 
282 fn beginPreparationStage(
283     allocator: std.mem.Allocator,
284     store: *revision.Store,
285     kind: *const revision.Kind,
286     source: SemanticInput,
287     parent: ?*const revision.Revision,
288     comptime stage: Stage,
289     request: PreparationRequest,
290     work: revision.receipt.Limits,
291 ) !*revision.Builder {
292     const bytes = try source.options(allocator, stage, request.options);
293     defer allocator.free(bytes);
294     const policy = try choir.product.recipe.encode(allocator, .{
295         .arithmetic = try source.arithmetic(),
296         .policy = "accy-preparation",
297     });
298     defer allocator.free(policy);
299     const dependencies: []const revision.store.Dependency = if (parent) |item|
300         &.{.{ .role = "source", .revision = item }}
301     else
302         &.{};
303     return store.begin(.{
304         .kind = kind,
305         .address = .{
306             .producer = "accy",
307             .source = request.source,
308             .stage = stage.name(),
309             .variant = request.variant,
310         },
311         .inputs = .{
312             .compiler_manifest = try choir.product.compiler.manifest(),
313             .versions = &.{ stage.schema(), operation.schema_identity, recipe.identity },
314             .pipeline = recipe.pipeline(stage),
315             .options = bytes,
316             .policy = policy,
317         },
318         .dependencies = dependencies,
319         .parent = parent,
320     }, work);
321 }
322 
323 fn prepareStage(
324     allocator: std.mem.Allocator,
325     builder: *revision.Builder,
326     kind: *const revision.Kind,
327     source: SemanticInput,
328     parent: ?*const revision.Revision,
329     comptime stage: Stage,
330     request: PreparationRequest,
331     comptime configuration: operation.Configuration,
332 ) !StageExecution {
333     if (stage == .semantic) {
334         switch (source) {
335             .draft => |draft| try draft.capture(builder, configuration),
336             .retained => |input| {
337                 if (try builder.admitReuse(input.record())) |reuse| return .{ .warm = reuse };
338                 return error.SemanticModuleRequired;
339             },
340         }
341     } else {
342         if (request.candidate) |candidate| {
343             if (try builder.admitReuse(candidate.stage(stage))) |reuse| return .{ .warm = reuse };
344         }
345         try capture(
346             allocator,
347             builder,
348             .{ .revision = parent.? },
349             stage,
350             request.options,
351             configuration,
352         );
353     }
354     return .{ .cold = try builder.seal(kind) };
355 }
356 
357 /// Builds one stage anew from the previous stage's record for the compile chain: the call runs the
358 /// stage's pass pipeline in a fresh compiler job bounded by the builder's limits, and copies the
359 /// result into the record builder, the object that collects one stage's result and is then sealed
360 /// into a stage record or aborted. The caller then seals or aborts the record builder, and owns any
361 /// diagnostics allocated with `diagnostic_allocator`. The call returns `error.WorkExhausted` when
362 /// the stage ran out of its limits, `error.MissingWorkContract` when a pass declared no bound, and
363 /// `error.PassFailed` for any other pipeline failure.
364 pub fn capture(
365     diagnostic_allocator: std.mem.Allocator,
366     builder: *revision.Builder,
367     parent: operation.Product,
368     comptime stage: Stage,
369     options: Options,
370     comptime configuration: operation.Configuration,
371 ) !void {
372     const work = builder.accounting();
373     captureJob(diagnostic_allocator, builder, parent, stage, options, configuration) catch |err| {
374         work.fail(if (err == error.WorkOverflow or err == error.WorkExhausted)
375             .exhausted
376         else
377             .rejected);
378         if (work.view().outcome == .exhausted) return error.WorkExhausted;
379         return err;
380     };
381 }
382 
383 fn captureJob(
384     diagnostic_allocator: std.mem.Allocator,
385     builder: *revision.Builder,
386     parent: operation.Product,
387     comptime stage: Stage,
388     options: Options,
389     comptime configuration: operation.Configuration,
390 ) !void {
391     if (stage == .semantic) @compileError("Semantic input has no predecessor pipeline");
392     const work = builder.accounting();
393     const job = try parent.successor(builder, configuration);
394     defer job.destroy();
395     errdefer if (job.exhausted()) work.fail(.exhausted);
396     var manager = choir.passes.PassManager.init(job.allocator());
397     defer manager.deinit();
398     try configure(&manager, builder, job, stage, &options);
399     var cache = try choir.passes.AnalysisCache.initAccounted(
400         job.allocator(),
401         &manager.stats,
402         work,
403         .{ .workspace = job.storageExhaustion(), .context = job.context() },
404         configuration.image.entities,
405     );
406     defer cache.deinit();
407     if (manager.runWithAnalysisCache(
408         job.root(0).?,
409         job.context(),
410         &cache,
411         recipe.runOptions(),
412     ) == .failure) {
413         execution.capturePipelineFailure(
414             diagnostic_allocator,
415             options.failure,
416             recipe.pipelineName(stage),
417             &manager,
418         );
419         if (work.view().outcome == .exhausted or job.exhausted()) {
420             return error.WorkExhausted;
421         }
422         if (work.view().missing_work_contract) return error.MissingWorkContract;
423         return error.PassFailed;
424     }
425     try work.producersComplete();
426     var context = choir.passes.PassContext.init(
427         job.root(0).?,
428         job.context(),
429         job.allocator(),
430         &cache,
431     );
432     defer context.deinit();
433     context.run_options = recipe.runOptions();
434     const bytes = try captureRecord(job, parent, &context, work, stage, configuration);
435     defer job.allocator().free(bytes);
436     try work.producersComplete();
437     try job.capture(builder, bytes, &.{}, configuration);
438 }
439 
440 fn configure(
441     manager: *choir.passes.PassManager,
442     builder: *revision.Builder,
443     job: *operation.Job,
444     comptime stage: Stage,
445     options: *const Options,
446 ) !void {
447     const work = builder.accounting();
448     const inputs = builder.inputs();
449     const token = try work.begin(.input, .{
450         .identity = .{ .name = "accy-stage-setup", .version = 1 },
451         .work = .{
452             .input_bytes = try bounds.add(
453                 try bounds.add(inputs.options.len, inputs.policy.len),
454                 try bounds.add(inputs.versions.bytes.len, inputs.pipeline.bytes.len),
455             ),
456             .output_bytes = job.storageCapacity(),
457             .structural_visits = try bounds.add(
458                 job.storageCapacity(),
459                 try bounds.add(inputs.versions.count, inputs.pipeline.count),
460             ),
461             .allocation_capacity = job.storageCapacity(),
462         },
463         .workspace = job.storageCapacity(),
464     });
465     errdefer {
466         if (job.exhausted()) work.fail(.exhausted);
467         work.finish(token, .rejected, .{}) catch {};
468     }
469     const root = job.root(0) orelse return error.InvalidStageRoots;
470     (try choir.product.recipe.decode(inputs.policy)).requireContext(job.context()) catch {
471         return error.RecipeMismatch;
472     };
473     if (stage == .kernel or stage == .target) {
474         try recipe.applyTargetOptions(job.allocator(), root, options.*);
475     }
476     const bytes = try recipe.encode(manager.allocator, stage, root, options.*);
477     defer manager.allocator.free(bytes);
478     if (!std.mem.eql(u8, inputs.options, bytes)) return error.RecipeMismatch;
479     try requirePipeline(inputs, stage);
480     try recipe.configure(manager, stage, options);
481     try work.finish(token, .success, .{ .work = .{ .output_bytes = bytes.len } });
482 }
483 
484 fn requirePipeline(inputs: revision.record.InputView, comptime stage: Stage) !void {
485     const expected = recipe.pipeline(stage);
486     if (inputs.pipeline.count != expected.len) return error.RecipeMismatch;
487     var pipeline = inputs.pipeline.iterator();
488     for (expected) |identity| {
489         if (!(try pipeline.next()).?.eql(identity)) return error.RecipeMismatch;
490     }
491     var versions = inputs.versions.iterator();
492     for (0..inputs.versions.count) |_| {
493         if ((try versions.next()).?.eql(recipe.identity)) return;
494     }
495     return error.RecipeMismatch;
496 }
497 
498 fn captureRecord(
499     job: *operation.Job,
500     parent: operation.Product,
501     context: *choir.passes.PassContext,
502     work: *revision.AccountingV1,
503     comptime stage: Stage,
504     comptime configuration: operation.Configuration,
505 ) ![]u8 {
506     const parent_bytes = if (stage == .target) parent.revision.view().exact.image.len else 0;
507     const visits = try bounds.multiply(
508         configuration.codec.fields,
509         try bounds.add(configuration.image.entities, parent_bytes),
510     );
511     const token = try work.begin(.capture, .{
512         .identity = .{ .name = "accy-stage-record", .version = 1 },
513         .work = .{
514             .input_bytes = try bounds.add(job.storageCapacity(), parent_bytes),
515             .output_bytes = job.storageCapacity(),
516             .structural_visits = visits,
517             .allocation_capacity = job.storageCapacity(),
518         },
519         .workspace = job.storageCapacity(),
520     });
521     errdefer {
522         if (job.exhausted()) work.fail(.exhausted);
523         work.finish(token, .rejected, .{}) catch {};
524     }
525     const bytes = try encodeRecord(job, parent, context, stage, configuration);
526     try work.finish(token, .success, .{ .work = .{ .output_bytes = bytes.len } });
527     return bytes;
528 }
529 
530 fn encodeRecord(
531     job: *operation.Job,
532     parent: operation.Product,
533     context: *choir.passes.PassContext,
534     comptime stage: Stage,
535     comptime configuration: operation.Configuration,
536 ) ![]u8 {
537     const allocator = job.allocator();
538     const root = job.root(0).?;
539     return switch (stage) {
540         .semantic, .contract, .tensor => allocator.dupe(u8, &publication.irRecord(stage)),
541         .dispatch => preparation.capture.dispatch(
542             allocator,
543             root,
544             try preparation.fusion.getFusionPlanAnalysis(context, root),
545             try preparation.schedule.getSchedulePlanAnalysis(context, root),
546             configuration.image.entities,
547         ),
548         .memory => preparation.capture.memory(
549             allocator,
550             root,
551             try preparation.bufferization.getBufferPlanAnalysis(context, root),
552             try preparation.memory.getMemorySpacePlanAnalysis(context, root),
553             try preparation.layout.getLayoutPlanAnalysis(context, root),
554             configuration.image.entities,
555         ),
556         .kernel => preparation.capture.kernel(
557             allocator,
558             root,
559             try preparation.outlining.getKernelOutlinePlanAnalysis(context, root),
560             try preparation.kernelization.getKernelizationAnalysis(context, root),
561             configuration,
562         ),
563         .target => captureTarget(job, parent, context, configuration),
564     };
565 }
566 
567 fn captureTarget(
568     job: *operation.Job,
569     parent: operation.Product,
570     context: *choir.passes.PassContext,
571     comptime configuration: operation.Configuration,
572 ) ![]u8 {
573     if (!std.mem.eql(u8, parent.revision.address().stage, Stage.kernel.name())) {
574         return error.WrongStage;
575     }
576     try parent.revision.requireGates(&.{Stage.kernel.schema()});
577     const allocator = job.allocator();
578     const image = try parent.open(allocator, configuration.image);
579     defer image.destroy();
580     var source = try records.codec.decode(allocator, records.kernel.Record, .kernel, image.stage());
581     defer source.deinit();
582     const Lowered = preparation.kernelization.LoweredKernel;
583     const kernels = try allocator.alloc(Lowered, source.value.generated.kernels.len);
584     defer allocator.free(kernels);
585     var initialized: usize = 0;
586     defer for (kernels[0..initialized]) |*kernel| kernel.deinit(allocator);
587     for (source.value.generated.kernels, kernels) |record, *kernel| {
588         kernel.* = try restoreKernel(allocator, record, configuration);
589         initialized += 1;
590     }
591     return preparation.capture.target(
592         allocator,
593         job.root(0).?,
594         try preparation.schedule.getSchedulePlanAnalysis(context, job.root(0).?),
595         kernels,
596         configuration,
597     );
598 }
599 
600 /// Restores a kernel from a sealed record back as a mutable kernel a caller can compile further:
601 /// the call rebuilds the captured kernel with its own copies of the entry name and the program,
602 /// allocated with `allocator`. The result owns those copies and is independent of the sealed
603 /// record.
604 pub fn restoreKernel(
605     allocator: std.mem.Allocator,
606     source: records.kernel.Lowered,
607     comptime configuration: operation.Configuration,
608 ) !preparation.kernelization.LoweredKernel {
609     const name = try allocator.dupe(u8, source.entry_name);
610     errdefer allocator.free(name);
611     const program = try records.program.restore(allocator, source.program, name, configuration);
612     var result: preparation.kernelization.LoweredKernel = undefined;
613     inline for (@typeInfo(@TypeOf(result)).@"struct".field_names) |field| {
614         @field(result, field) = if (comptime std.mem.eql(u8, field, "program"))
615             program
616         else if (comptime std.mem.eql(u8, field, "entry_name"))
617             name
618         else
619             @field(source, field);
620     }
621     return result;
622 }
623 
624 pub fn registerKind(
625     allocator: std.mem.Allocator,
626     store: *revision.Store,
627     comptime stage: Stage,
628     comptime configuration: operation.Configuration,
629 ) !*const revision.Kind {
630     return operation.registerKind(allocator, store, configuration, .{
631         .name = stage.name(),
632         .version = 1,
633     }, &.{.{
634         .identity = stage.schema(),
635         .definition = &.{ 1, 0, 0, 0, @backingInt(stage) },
636         .scratch_bytes = configuration.gate_scratch,
637         .run = struct {
638             fn run(input: revision.store.GateInput, scratch: []u8) !revision.record.EntityCounts {
639                 return verifyStage(input, scratch, stage, configuration);
640             }
641         }.run,
642     }});
643 }
644 
645 fn verifyStage(
646     input: revision.store.GateInput,
647     scratch: []u8,
648     comptime stage: Stage,
649     comptime configuration: operation.Configuration,
650 ) !revision.record.EntityCounts {
651     const address = try revision.record.decodeAddress(input.exact.address);
652     if (!std.mem.eql(u8, address.stage, stage.name())) return error.WrongStage;
653     try verifyPredecessor(input.dependencies, stage);
654     var reader = try revision.record.Reader.init(input.exact.image);
655     if (try reader.readInt(u32) != operation.image_version) return error.UnknownSchema;
656     if (try reader.readCount() != 1) return error.InvalidStageRoots;
657     const bytes = try reader.readBlob();
658     const stage_bytes = try reader.readBlob();
659     if (!reader.atEnd()) return error.InvalidStageRecord;
660     switch (stage) {
661         .semantic, .contract, .tensor => {
662             if (!std.mem.eql(u8, stage_bytes, &publication.irRecord(stage))) {
663                 return error.InvalidStageRecord;
664             }
665         },
666         else => {},
667     }
668     var memory = fixed.FixedBuffer.init(scratch);
669     const allocator = memory.allocator();
670     if (stage == .contract or stage == .tensor) {
671         try verifyDialects(allocator, input, bytes, configuration);
672     }
673     if (stage == .dispatch or stage == .memory or stage == .kernel or stage == .target) {
674         try verifyPlan(&memory, scratch, input, stage, stage_bytes, bytes, configuration);
675     }
676     return @splat(0);
677 }
678 
679 fn verifyDialects(
680     allocator: std.mem.Allocator,
681     input: revision.store.GateInput,
682     bytes: []const u8,
683     comptime configuration: operation.Configuration,
684 ) !void {
685     var context = try choir.ir.Context.init(allocator, configuration.context);
686     defer context.deinit(allocator);
687     const inputs = try revision.record.decodeInputs(input.exact.inputs);
688     (try choir.product.recipe.decode(inputs.policy)).restore(&context);
689     try configuration.register(&context);
690     var decoded = try choir.bytecode.decodeModule(allocator, &context, bytes);
691     defer decoded.deinit();
692     try @import("../choir/root.zig").contract.verifyAllowedDialects(decoded.module);
693 }
694 
695 fn verifyPredecessor(
696     dependencies: []const revision.record.Dependency,
697     comptime stage: Stage,
698 ) !void {
699     if (stage == .semantic) {
700         if (dependencies.len != 0) return error.InvalidStageDependency;
701         return;
702     }
703     const parent: Stage = switch (stage) {
704         .contract => .semantic,
705         .tensor => .contract,
706         .dispatch => .tensor,
707         .memory => .dispatch,
708         .kernel => .memory,
709         .target => .kernel,
710         else => unreachable,
711     };
712     if (dependencies.len != 1 or !std.mem.eql(u8, dependencies[0].role, "source")) {
713         return error.InvalidStageDependency;
714     }
715     const exact = try revision.record.decodeExact(dependencies[0].exact);
716     const address = try revision.record.decodeAddress(exact.address);
717     if (!std.mem.eql(u8, address.stage, parent.name())) return error.InvalidStageDependency;
718     const inputs = try revision.record.decodeInputs(exact.inputs);
719     try inputs.requireGateDeclarations(&.{ operation.schema_identity, parent.schema() });
720 }
721 
722 fn verifyPlan(
723     memory: *fixed.FixedBuffer,
724     scratch: []u8,
725     input: revision.store.GateInput,
726     comptime stage: Stage,
727     bytes: []const u8,
728     ir_bytes: []const u8,
729     comptime configuration: operation.Configuration,
730 ) !void {
731     const allocator = memory.allocator();
732     var plan = try records.codec.decode(allocator, publication.Plan(stage), stage, bytes);
733     defer plan.deinit();
734     const index = try choir.bytecode.image.Index.create(allocator, ir_bytes, configuration.image);
735     defer index.destroy();
736     try records.codec.validateReferences(plan.value, index.view());
737     switch (stage) {
738         .dispatch => try records.dispatch.validate(allocator, plan.value),
739         .memory => {
740             var dispatch = try predecessorPlan(allocator, input, .dispatch);
741             defer dispatch.deinit();
742             try records.memory.validate(allocator, plan.value, dispatch.value);
743         },
744         .kernel => {
745             var parent = try predecessorPlan(allocator, input, .memory);
746             defer parent.deinit();
747             try records.kernel.validate(allocator, plan.value, parent.value);
748             for (plan.value.generated.kernels) |kernel| {
749                 try verifyProgram(scratch[fixed.used(memory)..], kernel, configuration);
750             }
751         },
752         .target => {
753             var parent = try predecessorPlan(allocator, input, .kernel);
754             defer parent.deinit();
755             try records.target.validate(allocator, plan.value, parent.value);
756             for (plan.value.kernels) |kernel| {
757                 try verifyProgram(scratch[fixed.used(memory)..], kernel.lowered, configuration);
758             }
759         },
760         else => unreachable,
761     }
762 }
763 
764 fn verifyProgram(
765     scratch: []u8,
766     kernel: records.kernel.Lowered,
767     comptime configuration: operation.Configuration,
768 ) !void {
769     var memory = fixed.FixedBuffer.init(scratch);
770     try records.program.validate(
771         memory.allocator(),
772         kernel.program,
773         kernel.entry_name,
774         configuration,
775     );
776 }
777 
778 fn predecessorPlan(
779     allocator: std.mem.Allocator,
780     input: revision.store.GateInput,
781     comptime stage: Stage,
782 ) !records.codec.Decoded(publication.Plan(stage)) {
783     const parent = try revision.record.decodeExact(input.dependencies[0].exact);
784     var reader = try revision.record.Reader.init(parent.image);
785     if (try reader.readInt(u32) != operation.image_version) return error.UnknownSchema;
786     if (try reader.readCount() != 1) return error.InvalidStageRoots;
787     _ = try reader.readBlob();
788     const bytes = try reader.readBlob();
789     if (!reader.atEnd()) return error.InvalidStageRecord;
790     return records.codec.decode(allocator, publication.Plan(stage), stage, bytes);
791 }