lib/accy/src/choir/record/codec.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const choir = @import("choir");
  3 const reference = @import("root.zig").reference;
  4 const Stage = @import("../root.zig").publication.Stage;
  5 const binary = choir.serialization.binary;
  6 
  7 pub const limits = binary.Limits{
  8     .serialized_bytes = 64 * 1024 * 1024,
  9     .string_bytes = 64 * 1024 * 1024,
 10     .blob_bytes = 64 * 1024 * 1024,
 11     .collection_entries = 1024 * 1024,
 12     .total_entries = 4 * 1024 * 1024,
 13 };
 14 const Writer = binary.Writer(limits);
 15 const Reader = binary.Reader(limits);
 16 
 17 pub fn Decoded(comptime T: type) type {
 18     return struct {
 19         arena: std.heap.ArenaAllocator,
 20         value: T,
 21 
 22         pub fn deinit(self: *@This()) void {
 23             self.arena.deinit();
 24             self.* = undefined;
 25         }
 26     };
 27 }
 28 
 29 /// Writes a stage's plan into a stage record at the end of a stage for the
 30 /// compiler. The call writes version 1, the stage tag, and then every declared
 31 /// field of the record type `T`, read by name from `source`. Each pointer to an
 32 /// operation or value is written as its number from `references`, and a pointer
 33 /// absent from the reference index fails with `error.UnboundProductInput`. An
 34 /// integer exceeding its field fails with `error.InvalidStageRecord`, and the
 35 /// bytes are capped at 64 MiB with at most about a million entries per list and
 36 /// four million in all. The caller owns the returned bytes.
 37 pub fn encode(
 38     allocator: std.mem.Allocator,
 39     comptime T: type,
 40     stage: Stage,
 41     source: anytype,
 42     references: *const reference.Index,
 43 ) ![]u8 {
 44     var writer = Writer.init(allocator);
 45     defer writer.deinit();
 46     try writer.writeInt(u32, 1);
 47     try writer.writeTag(stage);
 48     try write(&writer, references, T, source, 0);
 49     return writer.finish();
 50 }
 51 
 52 /// Reads a stage's plan back out of a stage record. The call reads the bytes
 53 /// into a new value of `T` whose slices and bytes all live in an arena of the
 54 /// result, apart from `bytes`. The errors are `error.UnknownSchema` for a
 55 /// version other than 1, `error.WrongStage` for another stage's tag, and
 56 /// `error.InvalidStageRecord` for bytes left over. `Decoded.deinit` frees the
 57 /// arena and everything read into it.
 58 pub fn decode(allocator: std.mem.Allocator, comptime T: type, stage: Stage, bytes: []const u8) !Decoded(
 59     T,
 60 ) {
 61     var arena = std.heap.ArenaAllocator.init(allocator);
 62     errdefer arena.deinit();
 63     var reader = try Reader.init(bytes);
 64     if (try reader.readInt(u32) != 1) return error.UnknownSchema;
 65     if (try reader.readTag(Stage) != stage) return error.WrongStage;
 66     const value = try read(&reader, arena.allocator(), T, 0);
 67     if (!reader.atEnd()) return error.InvalidStageRecord;
 68     return .{ .arena = arena, .value = value };
 69 }
 70 
 71 fn write(
 72     writer: *Writer,
 73     refs: *const reference.Index,
 74     comptime T: type,
 75     source: anytype,
 76     comptime depth: u8,
 77 ) anyerror!void {
 78     if (depth == 32) @compileError("stage record type exceeds codec depth");
 79     if (T == reference.Operation and @TypeOf(source) != T) {
 80         return write(writer, refs, T, try refs.operation(source), depth + 1);
 81     }
 82     if (T == reference.Value and @TypeOf(source) != T) {
 83         return write(writer, refs, T, try refs.value(source), depth + 1);
 84     }
 85     const value = indirect(source);
 86     switch (@typeInfo(T)) {
 87         .int => try writer.writeInt(if (T == usize) u64 else T, std.math.cast(T, value) orelse
 88             return error.InvalidStageRecord),
 89         .float => |info| try writer.writeInt(@Int(.unsigned, info.bits), @bitCast(@as(T, value))),
 90         .bool => try writer.writeBool(value),
 91         .@"enum" => |info| if (info.mode == .nonexhaustive)
 92             try writer.writeInt(info.tag_type, @backingInt(@as(T, value)))
 93         else
 94             try writer.writeTag(@as(T, value)),
 95         .optional => |info| {
 96             try writer.writeBool(value != null);
 97             if (value) |present| try write(writer, refs, info.child, present, depth + 1);
 98         },
 99         .pointer => |info| {
100             if (info.size != .slice) @compileError("retained stage pointers must be slices");
101             const items = slice(value);
102             try writer.writeCount(items.len);
103             for (items) |item| try write(writer, refs, info.child, item, depth + 1);
104         },
105         .array => |info| for (value) |item| try write(writer, refs, info.child, item, depth + 1),
106         .@"struct" => |info| inline for (info.field_names, info.field_types) |name, Field| {
107             try write(writer, refs, Field, @field(value, name), depth + 1);
108         },
109         .@"union" => |info| {
110             const tag = std.meta.activeTag(value);
111             try writer.writeTag(tag);
112             inline for (info.field_names, info.field_types) |name, Field| {
113                 if (std.mem.eql(u8, @tagName(tag), name)) {
114                     try write(writer, refs, Field, @field(value, name), depth + 1);
115                     return;
116                 }
117             }
118             unreachable;
119         },
120         .void => {},
121         else => @compileError("unsupported stage record field"),
122     }
123 }
124 
125 fn read(
126     reader: *Reader,
127     allocator: std.mem.Allocator,
128     comptime T: type,
129     comptime depth: u8,
130 ) anyerror!T {
131     if (depth == 32) @compileError("stage record type exceeds codec depth");
132     return switch (@typeInfo(T)) {
133         .int => std.math.cast(T, try reader.readInt(if (T == usize) u64 else T)) orelse
134             error.InvalidStageRecord,
135         .float => |info| @bitCast(try reader.readInt(@Int(.unsigned, info.bits))),
136         .bool => reader.readBool(),
137         .@"enum" => |info| if (info.mode == .nonexhaustive)
138             @fromBackingInt(try reader.readInt(info.tag_type))
139         else
140             reader.readTag(T),
141         .optional => |info| if (try reader.readBool())
142             try read(reader, allocator, info.child, depth + 1)
143         else
144             null,
145         .pointer => |info| result: {
146             if (info.size != .slice) @compileError("retained stage pointers must be slices");
147             const items = try allocator.alloc(info.child, try reader.readCount());
148             for (items) |*item| item.* = try read(reader, allocator, info.child, depth + 1);
149             break :result items;
150         },
151         .array => |info| result: {
152             var items: T = undefined;
153             for (&items) |*item| item.* = try read(reader, allocator, info.child, depth + 1);
154             break :result items;
155         },
156         .@"struct" => |info| result: {
157             var value: T = undefined;
158             inline for (info.field_names, info.field_types) |name, Field| {
159                 @field(value, name) = try read(reader, allocator, Field, depth + 1);
160             }
161             break :result value;
162         },
163         .@"union" => |info| result: {
164             const tag = try reader.readTag(info.tag_type.?);
165             inline for (info.field_names, info.field_types) |name, Field| {
166                 if (std.mem.eql(u8, @tagName(tag), name)) {
167                     const payload = try read(reader, allocator, Field, depth + 1);
168                     break :result @unionInit(T, name, payload);
169                 }
170             }
171             unreachable;
172         },
173         .void => {},
174         else => @compileError("unsupported stage record field"),
175     };
176 }
177 
178 pub fn validateReferences(value: anytype, image: choir.bytecode.image.View) !void {
179     return validate(value, image, 0);
180 }
181 
182 fn validate(value: anytype, image: choir.bytecode.image.View, comptime depth: u8) anyerror!void {
183     if (depth == 32) @compileError("stage record type exceeds codec depth");
184     const T = @TypeOf(value);
185     if (T == reference.Operation) return value.validate(image);
186     if (T == reference.Value) {
187         _ = try value.ordinal(image);
188         return;
189     }
190     switch (@typeInfo(T)) {
191         .optional => if (value) |present| try validate(present, image, depth + 1),
192         .pointer, .array => for (value) |item| try validate(item, image, depth + 1),
193         .@"struct" => |info| inline for (info.field_names) |name| {
194             try validate(@field(value, name), image, depth + 1);
195         },
196         .@"union" => switch (value) {
197             inline else => |payload| try validate(payload, image, depth + 1),
198         },
199         else => {},
200     }
201 }
202 
203 /// Runs at compile time for each plan type stage capture code records. The call
204 /// stops the build when a field of `Source` is missing from `Record` and
205 /// omitted from `administrative`. The build also stops when a name in
206 /// `administrative` is absent from `Source` or is present in `Record`. A new
207 /// analysis field so has to be recorded or named as administrative before the
208 /// code compiles.
209 pub fn coverage(
210     comptime Source: type,
211     comptime Record: type,
212     comptime administrative: []const []const u8,
213 ) void {
214     inline for (@typeInfo(Source).@"struct".field_names) |name| {
215         if (!@hasField(Record, name)) {
216             const ignored = comptime for (administrative) |candidate| {
217                 if (std.mem.eql(u8, candidate, name)) break true;
218             } else false;
219             if (!ignored) {
220                 @compileError("uncaptured stage field: " ++ @typeName(Source) ++ "." ++ name);
221             }
222         }
223     }
224     inline for (administrative) |name| {
225         if (!@hasField(Source, name) or @hasField(Record, name)) {
226             @compileError("invalid administrative stage field: " ++ name);
227         }
228     }
229 }
230 
231 fn indirect(value: anytype) Indirect(@TypeOf(value)) {
232     return if (comptime @typeInfo(@TypeOf(value)) == .pointer and
233         @typeInfo(@TypeOf(value)).pointer.size == .one) value.* else value;
234 }
235 
236 fn Indirect(comptime T: type) type {
237     return if (@typeInfo(T) == .pointer and @typeInfo(T).pointer.size == .one)
238         @typeInfo(T).pointer.child
239     else
240         T;
241 }
242 
243 fn slice(value: anytype) Slice(@TypeOf(value)) {
244     return if (comptime @typeInfo(@TypeOf(value)) == .@"struct") value.items else value;
245 }
246 
247 fn Slice(comptime T: type) type {
248     return if (@typeInfo(T) == .@"struct") @FieldType(T, "items") else T;
249 }
250 
251 /// Proves that the stage record matches the live plan it came from, after stage
252 /// capture decodes its own output. The call walks the decoded value beside the
253 /// live `source` and returns `error.UnencodableProduct` at the first field that
254 /// differs. The walk has its own code path, apart from the encoder, so a fault
255 /// in the encoder shows up as a difference.
256 pub fn compare(value: anytype, source: anytype, refs: *const reference.Index) !void {
257     return compareField(value, source, refs, 0);
258 }
259 
260 fn compareField(
261     value: anytype,
262     source: anytype,
263     refs: *const reference.Index,
264     comptime depth: u8,
265 ) anyerror!void {
266     if (depth == 32) @compileError("stage record type exceeds codec depth");
267     const T = @TypeOf(value);
268     if (T == reference.Operation and @TypeOf(source) != T) {
269         return compareField(value, try refs.operation(source), refs, depth + 1);
270     }
271     if (T == reference.Value and @TypeOf(source) != T) {
272         return compareField(value, try refs.value(source), refs, depth + 1);
273     }
274     const expected = indirect(source);
275     switch (@typeInfo(T)) {
276         .int => if (value != (std.math.cast(T, expected) orelse return error.UnencodableProduct)) {
277             return error.UnencodableProduct;
278         },
279         .float => |info| {
280             const Bits = @Int(.unsigned, info.bits);
281             if (@as(Bits, @bitCast(value)) != @as(Bits, @bitCast(@as(T, expected)))) {
282                 return error.UnencodableProduct;
283             }
284         },
285         .bool, .@"enum" => if (value != expected) return error.UnencodableProduct,
286         .optional => {
287             if ((value == null) != (expected == null)) return error.UnencodableProduct;
288             if (value) |present| try compareField(present, expected.?, refs, depth + 1);
289         },
290         .pointer, .array => {
291             const items = slice(expected);
292             if (value.len != items.len) return error.UnencodableProduct;
293             for (value, items) |item, original| try compareField(item, original, refs, depth + 1);
294         },
295         .@"struct" => |info| inline for (info.field_names) |name| {
296             try compareField(@field(value, name), @field(expected, name), refs, depth + 1);
297         },
298         .@"union" => {
299             if (std.meta.activeTag(value) != std.meta.activeTag(expected)) {
300                 return error.UnencodableProduct;
301             }
302             switch (value) {
303                 inline else => |payload, tag| {
304                     try compareField(payload, @field(expected, @tagName(tag)), refs, depth + 1);
305                 },
306             }
307         },
308         .void => {},
309         else => @compileError("unsupported stage record field"),
310     }
311 }