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 }