lib/accy/src/preparation/fingerprint.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 //! Functions that reduce three plans of one compile to a 64-bit number each: how operations are
  2 //! grouped into launches and scheduled, how buffers are placed in memory, and how the functions a
  3 //! device runs across many threads at once are outlined and generated.
  4 //!
  5 //! A person comparing two compiles wants to see at a glance whether a middle stage planned the same
  6 //! thing, without reading the plans. A short number that looks like an identity invites code to use
  7 //! it as one, and a summary that depends on function names or on where values sit in memory changes
  8 //! between two compiles of the same plan.
  9 //!
 10 //! Each summary is written into the per-stage display record of one compile, built for people
 11 //! reading it (*run stamp*). No product key, reuse check, receipt identity or other decision reads
 12 //! a summary, because the sealed record of each stage (its *stage record*) holds the exact plan.
 13 //! Values are numbered by a stable order within the module, and operations are summarized by the
 14 //! shape of the tree beneath them, so equal plans inside functions with different names give equal
 15 //! summaries. A planned buffer (a *buffer slot*) records only whether it has an enclosing function,
 16 //! because the value it holds already identifies that function. Every field of every plan is either
 17 //! summarized or listed as administrative, and a compile-time check stops the build when a new
 18 //! field is neither.
 19 
 20 const std = @import("std");
 21 const choir = @import("choir");
 22 
 23 const accy_choir = @import("../choir/root.zig");
 24 const bufferization = @import("bufferization/root.zig");
 25 const fusion = @import("fusion/root.zig");
 26 const kernelization = @import("kernelization/root.zig");
 27 const layout = @import("layout.zig");
 28 const memory_space = @import("memory.zig");
 29 const outlining = @import("outlining/root.zig");
 30 const schedule_planning = @import("schedule/root.zig");
 31 
 32 const ir = choir.ir;
 33 const outline_model = kernelization.product;
 34 const Builder = choir.product.incremental.FingerprintBuilder;
 35 const Numbering = choir.StableValueNumbering;
 36 
 37 /// Returns one 64-bit summary of the fusion plan and the schedule plan of `module`, taken after the
 38 /// dispatch pipeline, for the run record. The summary also covers the product name and
 39 /// `pipeline_name`. The allocator backs only scratch for value numbering, which is freed before the
 40 /// call returns.
 41 pub fn dispatch(
 42     allocator: std.mem.Allocator,
 43     module: *accy_choir.tensor.TensorJob,
 44     pipeline_name: []const u8,
 45 ) !u64 {
 46     comptime dispatchCoverage();
 47     var pass_ctx = module.passContext();
 48     defer pass_ctx.deinit();
 49     const root = module.choir_module;
 50     const fusion_plan = try fusion.getFusionPlanAnalysis(&pass_ctx, root);
 51     const schedule_plan = try schedule_planning.getSchedulePlanAnalysis(&pass_ctx, root);
 52 
 53     const limits = try Numbering.Limits.inspect(root);
 54     var numbering = try Numbering.init(allocator, limits);
 55     defer numbering.deinit(allocator);
 56     try numbering.activate();
 57 
 58     var builder = Builder{};
 59     builder.updateBytes(accy_choir.dispatch.product_name);
 60     builder.updateBytes(pipeline_name);
 61     updateFusionPlan(&builder, fusion_plan, &numbering);
 62     updateSchedulePlan(&builder, schedule_plan, &numbering);
 63     return builder.finish();
 64 }
 65 
 66 /// Returns one 64-bit summary of the buffer plan, the memory-space plan and the layout plan of
 67 /// `module`, taken after the memory pipeline, for the run record. The summary also covers the
 68 /// product name and `pipeline_name`. The allocator backs only scratch for value numbering and the
 69 /// buffer plan walk, which is freed before the call returns.
 70 pub fn memory(
 71     allocator: std.mem.Allocator,
 72     module: *accy_choir.dispatch.DispatchJob,
 73     pipeline_name: []const u8,
 74 ) !u64 {
 75     comptime memoryCoverage();
 76     var pass_ctx = module.passContext();
 77     defer pass_ctx.deinit();
 78     const root = module.choir_module;
 79     const buffer_plan = try bufferization.getBufferPlanAnalysis(&pass_ctx, root);
 80     const space_plan = try memory_space.getMemorySpacePlanAnalysis(&pass_ctx, root);
 81     const layout_plan = try layout.getLayoutPlanAnalysis(&pass_ctx, root);
 82 
 83     const limits = try Numbering.Limits.inspect(root);
 84     var numbering = try Numbering.init(allocator, limits);
 85     defer numbering.deinit(allocator);
 86     try numbering.activate();
 87 
 88     var builder = Builder{};
 89     builder.updateBytes(accy_choir.memory.product_name);
 90     builder.updateBytes(pipeline_name);
 91     try updateBufferPlan(allocator, &builder, buffer_plan, &numbering);
 92     updateSpacePlan(&builder, space_plan, &numbering);
 93     updateLayoutPlan(&builder, layout_plan, &numbering);
 94     return builder.finish();
 95 }
 96 
 97 /// Returns one 64-bit summary of the kernel outline plan and the generated kernels of `module`,
 98 /// taken after the kernel pipeline, for the run record. The summary also covers the product name
 99 /// and `pipeline_name`. The names of the generated kernels leave the summary unchanged.
100 pub fn kernel(
101     allocator: std.mem.Allocator,
102     module: *accy_choir.memory.MemoryJob,
103     pipeline_name: []const u8,
104 ) !u64 {
105     comptime kernelCoverage();
106     var pass_ctx = module.passContext();
107     defer pass_ctx.deinit();
108     const root = module.choir_module;
109     const outline_plan = try outlining.getKernelOutlinePlanAnalysis(&pass_ctx, root);
110     const kernel_plan = try kernelization.getKernelizationAnalysis(&pass_ctx, root);
111 
112     const limits = try Numbering.Limits.inspect(root);
113     var numbering = try Numbering.init(allocator, limits);
114     defer numbering.deinit(allocator);
115     try numbering.activate();
116 
117     var builder = Builder{};
118     builder.updateBytes(accy_choir.gpu.product_name);
119     builder.updateBytes(pipeline_name);
120     updateOutlinePlan(&builder, outline_plan, &numbering);
121     try updateKernelPlan(allocator, &builder, kernel_plan);
122     return builder.finish();
123 }
124 
125 fn updateFusionPlan(
126     builder: *Builder,
127     plan: *const fusion.FusionPlanAnalysis,
128     numbering: *Numbering,
129 ) void {
130     builder.updateBytes(fusion.fusion_plan_analysis_name);
131     builder.updateUsize(plan.clusters.items.len);
132     builder.updateUsize(plan.fused_op_count);
133     builder.updateUsize(plan.max_cluster_len);
134     for (plan.clusters.items) |cluster| {
135         builder.updateEnumTag(cluster.kind);
136         updateOperations(builder, numbering, cluster.ops);
137     }
138     updateOperations(builder, numbering, plan.elided.items);
139 }
140 
141 fn updateSchedulePlan(
142     builder: *Builder,
143     plan: *const schedule_planning.SchedulePlanAnalysis,
144     numbering: *Numbering,
145 ) void {
146     builder.updateBytes(schedule_planning.schedule_plan_analysis_name);
147     builder.updateUsize(plan.work_items.items.len);
148     builder.updateUsize(plan.single_work_count);
149     builder.updateUsize(plan.fusion_work_count);
150     builder.updateUsize(plan.kernel_call_work_count);
151     builder.updateUsize(plan.scheduled_op_count);
152     builder.updateU64(plan.total_static_elements);
153     for (plan.work_items.items) |work| {
154         builder.updateUsize(work.id);
155         builder.updateEnumTag(work.kind);
156         numbering.updateSubtreeFingerprint(builder, work.root);
157         updateOperations(builder, numbering, work.ops);
158         updateValue(builder, numbering, work.output_value);
159         builder.updateU64(@backingInt(work.dtype));
160         builder.updateUsize(work.rank);
161         builder.updateU64(work.element_count);
162         updateResources(builder, work.resources);
163     }
164 }
165 
166 fn updateResources(
167     builder: *Builder,
168     resources: schedule_planning.ScheduleResourceEstimate,
169 ) void {
170     builder.updateU64(resources.element_count);
171     builder.updateU64(resources.element_size);
172     builder.updateUsize(resources.op_count);
173     builder.updateUsize(resources.external_input_value_count);
174     builder.updateUsize(resources.external_operand_count);
175     builder.updateUsize(resources.chain_operand_count);
176     builder.updateU64(resources.static_read_bytes);
177     builder.updateU64(resources.static_write_bytes);
178     builder.updateU64(resources.static_total_bytes);
179     builder.updateU64(resources.estimated_element_ops);
180     builder.updateBool(resources.static_bytes_complete);
181 }
182 
183 fn updateBufferPlan(
184     allocator: std.mem.Allocator,
185     builder: *Builder,
186     plan: *const bufferization.BufferPlanAnalysis,
187     numbering: *Numbering,
188 ) !void {
189     builder.updateBytes(bufferization.buffer_plan_analysis_name);
190     builder.updateUsize(plan.slots.items.len);
191     builder.updateUsize(plan.elisions.items.len);
192     builder.updateUsize(plan.input_slot_count);
193     builder.updateUsize(plan.output_slot_count);
194     builder.updateUsize(plan.temporary_slot_count);
195     builder.updateUsize(plan.constant_slot_count);
196     builder.updateUsize(plan.dynamic_slot_count);
197     builder.updateU64(plan.total_static_bytes);
198     for (plan.slots.items) |slot| {
199         builder.updateUsize(slot.id);
200         updateValue(builder, numbering, slot.value);
201         updateOptionalOperation(builder, numbering, slot.producer);
202         builder.updateBool(slot.function != null);
203         updateRole(builder, slot.role);
204         builder.updateU64(@backingInt(slot.dtype));
205         builder.updateI64Slice(slot.dims);
206         builder.updateOptionalU64(slot.element_count);
207         builder.updateOptionalU64Slice(slot.row_major_strides);
208         builder.updateOptionalU64(slot.byte_size);
209     }
210     for (plan.elisions.items) |elision| {
211         updateValue(builder, numbering, elision.value);
212         numbering.updateSubtreeFingerprint(builder, elision.producer);
213         numbering.updateSubtreeFingerprint(builder, elision.root);
214         builder.updateUsize(elision.cluster_index);
215     }
216     try updateBindings(allocator, builder, plan, numbering);
217 }
218 
219 const Binding = struct {
220     value: u64,
221     slot_id: usize,
222 
223     fn lessThan(_: void, lhs: Binding, rhs: Binding) bool {
224         if (lhs.value != rhs.value) return lhs.value < rhs.value;
225         return lhs.slot_id < rhs.slot_id;
226     }
227 };
228 
229 fn updateBindings(
230     allocator: std.mem.Allocator,
231     builder: *Builder,
232     plan: *const bufferization.BufferPlanAnalysis,
233     numbering: *const Numbering,
234 ) !void {
235     const bindings = try allocator.alloc(Binding, plan.value_to_slot.count());
236     defer allocator.free(bindings);
237     var entries = plan.value_to_slot.iterator();
238     var count: usize = 0;
239     while (entries.next()) |entry| : (count += 1) {
240         bindings[count] = .{
241             .value = numbering.valueId(entry.key_ptr.*) orelse std.math.maxInt(u64),
242             .slot_id = entry.value_ptr.*,
243         };
244     }
245     std.debug.assert(count == bindings.len);
246     std.mem.sort(Binding, bindings, {}, Binding.lessThan);
247     builder.updateUsize(bindings.len);
248     for (bindings) |binding| {
249         builder.updateU64(binding.value);
250         builder.updateUsize(binding.slot_id);
251     }
252 }
253 
254 fn updateSpacePlan(
255     builder: *Builder,
256     plan: *const memory_space.MemorySpacePlanAnalysis,
257     numbering: *Numbering,
258 ) void {
259     builder.updateBytes(memory_space.memory_space_plan_analysis_name);
260     builder.updateUsize(plan.assignments.items.len);
261     builder.updateUsize(plan.host_slot_count);
262     builder.updateUsize(plan.device_global_slot_count);
263     builder.updateUsize(plan.device_constant_slot_count);
264     builder.updateUsize(plan.device_shared_slot_count);
265     builder.updateUsize(plan.unified_slot_count);
266     builder.updateUsize(plan.host_input_transfer_count);
267     builder.updateUsize(plan.host_output_transfer_count);
268     builder.updateUsize(plan.dynamic_slot_count);
269     builder.updateUsize(plan.elided_value_count);
270     builder.updateU64(plan.total_static_bytes);
271     for (plan.assignments.items) |assignment| {
272         builder.updateUsize(assignment.slot_id);
273         updateValue(builder, numbering, assignment.value);
274         updateOptionalOperation(builder, numbering, assignment.producer);
275         updateRole(builder, assignment.role);
276         builder.updateEnumTag(assignment.space);
277         builder.updateEnumTag(assignment.access);
278         builder.updateEnumTag(assignment.transfer);
279         builder.updateOptionalU64(assignment.byte_size);
280         updateOutputSource(builder, assignment.output_source);
281     }
282 }
283 
284 fn updateOutputSource(builder: *Builder, source: ?memory_space.OutputSource) void {
285     if (source) |value| {
286         builder.updateBool(true);
287         builder.updateEnumTag(std.meta.activeTag(value));
288         switch (value) {
289             inline else => |index| builder.updateUsize(index),
290         }
291     } else {
292         builder.updateBool(false);
293     }
294 }
295 
296 fn updateLayoutPlan(
297     builder: *Builder,
298     plan: *const layout.LayoutPlanAnalysis,
299     numbering: *Numbering,
300 ) void {
301     builder.updateBytes(layout.layout_plan_analysis_name);
302     builder.updateUsize(plan.assignments.items.len);
303     builder.updateUsize(plan.scalar_layout_count);
304     builder.updateUsize(plan.row_major_layout_count);
305     builder.updateUsize(plan.dynamic_row_major_layout_count);
306     builder.updateUsize(plan.host_slot_count);
307     builder.updateUsize(plan.device_global_slot_count);
308     builder.updateUsize(plan.device_constant_slot_count);
309     builder.updateUsize(plan.device_shared_slot_count);
310     builder.updateUsize(plan.unified_slot_count);
311     builder.updateUsize(plan.dynamic_slot_count);
312     builder.updateUsize(plan.elided_value_count);
313     builder.updateU64(plan.total_static_bytes);
314     for (plan.assignments.items) |assignment| {
315         updateLayout(builder, assignment, numbering);
316     }
317 }
318 
319 fn updateLayout(
320     builder: *Builder,
321     assignment: layout.LayoutAssignment,
322     numbering: *Numbering,
323 ) void {
324     builder.updateUsize(assignment.slot_id);
325     updateValue(builder, numbering, assignment.value);
326     updateOptionalOperation(builder, numbering, assignment.producer);
327     updateRole(builder, assignment.role);
328     builder.updateU64(@backingInt(assignment.dtype));
329     builder.updateEnumTag(assignment.memory_space);
330     builder.updateEnumTag(assignment.kind);
331     builder.updateUsize(assignment.rank);
332     builder.updateI64Slice(assignment.dims);
333     builder.updateOptionalU64Slice(assignment.element_strides);
334     builder.updateUsizeSlice(assignment.minor_to_major);
335     builder.updateOptionalU64(assignment.element_count);
336     builder.updateOptionalU64(assignment.byte_size);
337     builder.updateU64(assignment.element_size);
338     builder.updateU64(assignment.alignment);
339     builder.updateBool(assignment.contiguous);
340     builder.updateBool(assignment.static_layout);
341 }
342 
343 fn updateOutlinePlan(
344     builder: *Builder,
345     plan: *const outline_model.KernelOutlinePlanAnalysis,
346     numbering: *Numbering,
347 ) void {
348     builder.updateBytes(outline_model.kernel_outline_plan_analysis_name);
349     builder.updateUsize(plan.kernels.items.len);
350     builder.updateUsize(plan.total_input_slots);
351     builder.updateUsize(plan.total_scheduled_ops);
352     for (plan.kernels.items) |outline| {
353         builder.updateUsize(outline.id);
354         builder.updateBytes(outline.name);
355         builder.updateEnumTag(outline.kind);
356         builder.updateUsize(outline.work_item_id);
357         numbering.updateSubtreeFingerprint(builder, outline.root);
358         builder.updateUsizeSlice(outline.input_slot_ids);
359         builder.updateUsize(outline.output_slot_id);
360         builder.updateU64(outline.element_count);
361         builder.updateUsize(outline.op_count);
362     }
363 }
364 
365 fn updateKernelPlan(
366     allocator: std.mem.Allocator,
367     builder: *Builder,
368     plan: *const kernelization.KernelizationAnalysis,
369 ) !void {
370     builder.updateBytes(kernelization.kernelization_analysis_name);
371     builder.updateUsize(plan.kernels.items.len);
372     for (plan.kernels.items) |*lowered| {
373         builder.updateUsize(lowered.work_item_id);
374         builder.updateBytes(lowered.entry_name);
375         builder.updateU64(try lowered.program.fingerprint(allocator));
376         builder.updateU32(lowered.argument_count);
377         builder.updateU64(lowered.body_fingerprint);
378         builder.updateU32(lowered.dynamic_shared_memory_bytes);
379         builder.updateEnumTag(lowered.schedule.kind);
380         builder.updateU32(lowered.schedule.threads.x);
381         builder.updateU32(lowered.schedule.threads.y);
382         builder.updateU32(lowered.schedule.threads.z);
383         if (lowered.launch) |launch| {
384             builder.updateBool(true);
385             for (launch.grid) |extent| builder.updateU32(extent);
386             for (launch.block) |extent| builder.updateU32(extent);
387         } else {
388             builder.updateBool(false);
389         }
390         builder.updateOptionalU32(lowered.output_fill_pattern);
391         builder.updateOptionalU32(lowered.scratch_fill_pattern);
392         updateBody(builder, lowered.body);
393     }
394 }
395 
396 fn updateBody(builder: *Builder, body: kernelization.LoweredKernelBody) void {
397     builder.updateEnumTag(std.meta.activeTag(body));
398     switch (body) {
399         .generic => {},
400         inline else => |payload| updateScalars(builder, payload),
401     }
402 }
403 
404 fn updateScalars(builder: *Builder, value: anytype) void {
405     const Value = @TypeOf(value);
406     switch (@typeInfo(Value)) {
407         .int => builder.updateU64(value),
408         .@"struct" => |info| inline for (info.field_names) |name| {
409             builder.updateU64(@field(value, name));
410         },
411         else => @compileError("unsummarized kernel body payload: " ++ @typeName(Value)),
412     }
413 }
414 
415 fn updateOperations(
416     builder: *Builder,
417     numbering: *Numbering,
418     operations: []const *ir.Operation,
419 ) void {
420     builder.updateUsize(operations.len);
421     for (operations) |operation| numbering.updateSubtreeFingerprint(builder, operation);
422 }
423 
424 fn updateOptionalOperation(
425     builder: *Builder,
426     numbering: *Numbering,
427     operation: ?*ir.Operation,
428 ) void {
429     if (operation) |value| {
430         builder.updateBool(true);
431         numbering.updateSubtreeFingerprint(builder, value);
432     } else {
433         builder.updateBool(false);
434     }
435 }
436 
437 fn updateValue(builder: *Builder, numbering: *const Numbering, value: *const ir.Value) void {
438     if (numbering.valueId(value)) |id| {
439         builder.updateBool(true);
440         builder.updateU64(id);
441     } else {
442         builder.updateBool(false);
443         builder.updateU64(std.math.maxInt(u64));
444     }
445 }
446 
447 fn updateRole(builder: *Builder, role: bufferization.BufferRole) void {
448     builder.updateBool(role.input);
449     builder.updateBool(role.output);
450     builder.updateBool(role.temporary);
451     builder.updateBool(role.constant);
452 }
453 
454 fn dispatchCoverage() void {
455     coverage(fusion.FusionCluster, &.{ "ops", "kind" }, &.{});
456     coverage(fusion.FusionPlanAnalysis, &.{
457         "clusters", "elided", "fused_op_count", "max_cluster_len",
458     }, &.{"allocator"});
459     coverage(schedule_planning.ScheduleWorkItem, &.{
460         "id",    "kind", "root",          "ops",       "output_value",
461         "dtype", "rank", "element_count", "resources",
462     }, &.{});
463     coverage(schedule_planning.ScheduleResourceEstimate, &.{
464         "element_count",              "element_size",           "op_count",
465         "external_input_value_count", "external_operand_count", "chain_operand_count",
466         "static_read_bytes",          "static_write_bytes",     "static_total_bytes",
467         "estimated_element_ops",      "static_bytes_complete",
468     }, &.{});
469     coverage(schedule_planning.SchedulePlanAnalysis, &.{
470         "work_items",             "single_work_count",  "fusion_work_count",
471         "kernel_call_work_count", "scheduled_op_count", "total_static_elements",
472     }, &.{ "allocator", "root_to_item" });
473 }
474 
475 fn memoryCoverage() void {
476     coverage(bufferization.BufferRole, &.{ "input", "output", "temporary", "constant" }, &.{});
477     coverage(bufferization.BufferSlot, &.{
478         "id",    "value", "producer",      "function",          "role",
479         "dtype", "dims",  "element_count", "row_major_strides", "byte_size",
480     }, &.{});
481     coverage(bufferization.FusionElision, &.{
482         "value", "producer", "root", "cluster_index",
483     }, &.{});
484     coverage(bufferization.BufferPlanAnalysis, &.{
485         "slots",               "elisions",           "value_to_slot",
486         "input_slot_count",    "output_slot_count",  "temporary_slot_count",
487         "constant_slot_count", "dynamic_slot_count", "total_static_bytes",
488     }, &.{ "allocator", "value_to_elision" });
489     coverage(memory_space.MemorySpaceAssignment, &.{
490         "slot_id", "value",    "producer",  "role",          "space",
491         "access",  "transfer", "byte_size", "output_source",
492     }, &.{});
493     coverage(memory_space.MemorySpacePlanAnalysis, &.{
494         "assignments",                "host_slot_count",            "device_global_slot_count",
495         "device_constant_slot_count", "device_shared_slot_count",   "unified_slot_count",
496         "host_input_transfer_count",  "host_output_transfer_count", "dynamic_slot_count",
497         "elided_value_count",         "total_static_bytes",
498     }, &.{ "allocator", "slot_to_assignment" });
499     coverage(layout.LayoutAssignment, &.{
500         "slot_id",        "value",         "producer",  "role",         "dtype",
501         "memory_space",   "kind",          "rank",      "dims",         "element_strides",
502         "minor_to_major", "element_count", "byte_size", "element_size", "alignment",
503         "contiguous",     "static_layout",
504     }, &.{});
505     coverage(layout.LayoutPlanAnalysis, &.{
506         "assignments",                    "scalar_layout_count",      "row_major_layout_count",
507         "dynamic_row_major_layout_count", "host_slot_count",          "device_global_slot_count",
508         "device_constant_slot_count",     "device_shared_slot_count", "unified_slot_count",
509         "dynamic_slot_count",             "elided_value_count",       "total_static_bytes",
510     }, &.{ "allocator", "slot_to_assignment" });
511 }
512 
513 fn kernelCoverage() void {
514     coverage(outline_model.KernelOutline, &.{
515         "id",             "name",           "kind",          "work_item_id", "root",
516         "input_slot_ids", "output_slot_id", "element_count", "op_count",
517     }, &.{});
518     coverage(outline_model.KernelOutlinePlanAnalysis, &.{
519         "kernels", "total_input_slots", "total_scheduled_ops",
520     }, &.{ "allocator", "work_to_kernel" });
521     coverage(kernelization.LoweredKernel, &.{
522         "work_item_id",         "entry_name",       "program",
523         "argument_count",       "body_fingerprint", "dynamic_shared_memory_bytes",
524         "schedule",             "launch",           "output_fill_pattern",
525         "scratch_fill_pattern", "body",
526     }, &.{});
527     coverage(kernelization.GeneratedSchedule, &.{ "kind", "threads" }, &.{});
528     coverage(@FieldType(kernelization.GeneratedSchedule, "threads"), &.{ "x", "y", "z" }, &.{});
529     const Launch = @typeInfo(@FieldType(kernelization.LoweredKernel, "launch")).optional.child;
530     coverage(Launch, &.{ "grid", "block" }, &.{});
531     coverage(kernelization.KernelizationAnalysis, &.{"kernels"}, &.{
532         "allocator", "context", "work_to_kernel",
533     });
534 }
535 
536 fn coverage(
537     comptime Plan: type,
538     comptime summarized: []const []const u8,
539     comptime administrative: []const []const u8,
540 ) void {
541     @setEvalBranchQuota(100_000);
542     const names = @typeInfo(Plan).@"struct".field_names;
543     if (names.len != summarized.len + administrative.len) {
544         @compileError("stale plan summary field list: " ++ @typeName(Plan));
545     }
546     inline for (names) |name| {
547         if (!listed(summarized, name) and !listed(administrative, name)) {
548             @compileError("unsummarized plan field: " ++ @typeName(Plan) ++ "." ++ name);
549         }
550     }
551 }
552 
553 fn listed(comptime names: []const []const u8, comptime name: []const u8) bool {
554     for (names) |candidate| {
555         if (std.mem.eql(u8, candidate, name)) return true;
556     }
557     return false;
558 }