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 }