lib/choir/src/product/revision/record.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const simd = @import("simd");
3 const binary = @import("../../serialization/binary/root.zig");
4
5 const Bytes = simd.ScalableTag(u8);
6
7 pub const schema_version: u32 = 2;
8
9 /// Maximum number of nested values in a semantic projection.
10 pub const maximum_projection_depth: u8 = 64;
11 pub const ProjectionError = binary.WriteError || error{UnencodableProduct};
12 pub const codec_limits = binary.Limits{
13 .serialized_bytes = std.math.maxInt(u32),
14 .string_bytes = std.math.maxInt(u32),
15 .blob_bytes = std.math.maxInt(u32),
16 .collection_entries = std.math.maxInt(u32),
17 .total_entries = std.math.maxInt(u32),
18 };
19 pub const Writer = binary.Writer(codec_limits);
20 pub const Reader = binary.Reader(codec_limits);
21
22 pub const Version = struct {
23 name: []const u8,
24 version: u32,
25
26 pub fn eql(self: Version, other: Version) bool {
27 return self.version == other.version and std.mem.eql(u8, self.name, other.name);
28 }
29 };
30
31 pub const Address = struct {
32 producer: []const u8,
33 source: []const u8,
34 stage: []const u8,
35 variant: []const u8,
36
37 pub fn eql(self: Address, other: Address) bool {
38 inline for (@typeInfo(Address).@"struct".field_names) |field| {
39 if (!std.mem.eql(u8, @field(self, field), @field(other, field))) return false;
40 }
41 return true;
42 }
43 };
44
45 /// A missing value records a negative observation, distinct from empty bytes.
46 pub const Fact = struct {
47 key: []const u8,
48 value: ?[]const u8,
49
50 pub fn eql(self: Fact, other: Fact) bool {
51 if (!std.mem.eql(u8, self.key, other.key)) return false;
52 if (self.value) |value| {
53 return std.mem.eql(u8, value, other.value orelse return false);
54 }
55 return other.value == null;
56 }
57 };
58
59 /// Dependency records contain exact semantic bytes, excluding publication events.
60 pub const Dependency = struct {
61 role: []const u8,
62 exact: []const u8,
63 };
64
65 /// The compiler manifest is admission input, not encoded content. An owner
66 /// interns it and decides identity through the interned bytes, so `encodeInputs`
67 /// reads every other field and leaves this one to the store.
68 pub const Inputs = struct {
69 compiler_manifest: []const u8,
70 versions: []const Version,
71 pipeline: []const Version,
72 options: []const u8,
73 policy: []const u8,
74 facts: []const Fact = &.{},
75 };
76
77 /// Decoded input fields. A record does not carry its compiler manifest, so this
78 /// view cannot answer which compiler produced it; ask the owner that holds it.
79 pub const InputView = struct {
80 versions: Entries(Version),
81 pipeline: Entries(Version),
82 options: []const u8,
83 policy: []const u8,
84 facts: Entries(Fact),
85 dependencies: Entries(Dependency),
86 gate_manifest: []const u8,
87
88 /// Checks declared gate identities only. Transported bytes do not establish
89 /// that gates ran; callers obtain publication authority from a live Revision.
90 pub fn requireGateDeclarations(self: InputView, required: []const Version) !void {
91 for (required) |identity| {
92 if (!try declaresGate(self.gate_manifest, identity)) return error.MissingGateEvidence;
93 }
94 }
95 };
96
97 fn declaresGate(bytes: []const u8, required: Version) !bool {
98 var reader = try Reader.init(bytes);
99 if (try reader.readInt(u32) != schema_version) return error.UnknownSchema;
100 _ = try reader.readString();
101 _ = try reader.readInt(u32);
102 var found = (try readGateIdentity(&reader)).eql(required);
103 const count = try reader.readCount();
104 for (0..count) |_| {
105 const identity = try readGateIdentity(&reader);
106 found = identity.eql(required) or found;
107 }
108 if (!reader.atEnd()) return error.InvalidRecord;
109 return found;
110 }
111
112 fn readGateIdentity(reader: *Reader) !Version {
113 const identity = Version{
114 .name = try reader.readString(),
115 .version = try reader.readInt(u32),
116 };
117 _ = try reader.readBlob();
118 _ = try reader.readInt(u32);
119 return identity;
120 }
121
122 pub const ClosureBounds = struct {
123 bytes: u32,
124 records: u32,
125 depth: u8,
126 };
127
128 const ClosureFrame = struct {
129 dependencies: Entries(Dependency).Iterator,
130 };
131
132 /// Embedded dependencies strictly decrease in byte extent, so exact cycles cannot
133 /// be represented. The iterative walk validates every closed record under bounds.
134 pub fn validateClosure(bytes: []const u8, bounds: ClosureBounds) !void {
135 if (bytes.len > bounds.bytes) return error.RecordLimit;
136 if (bounds.records == 0 or bounds.depth == 0) return error.RecordClosureLimit;
137 var stack: [255]ClosureFrame = undefined;
138 stack[0] = try closureFrame(bytes);
139 var depth: usize = 1;
140 var count: u32 = 1;
141 while (depth != 0) {
142 const dependency = try stack[depth - 1].dependencies.next() orelse {
143 depth -= 1;
144 continue;
145 };
146 if (count == bounds.records or depth == bounds.depth) return error.RecordClosureLimit;
147 stack[depth] = try closureFrame(dependency.exact);
148 depth += 1;
149 count += 1;
150 }
151 }
152
153 fn closureFrame(bytes: []const u8) !ClosureFrame {
154 const exact = try decodeExact(bytes);
155 _ = try decodeAddress(exact.address);
156 const inputs = try decodeInputs(exact.inputs);
157 return .{ .dependencies = inputs.dependencies.iterator() };
158 }
159
160 pub fn Entries(comptime T: type) type {
161 return struct {
162 bytes: []const u8,
163 count: u32,
164
165 pub fn iterator(self: @This()) Iterator {
166 return .{
167 .reader = Reader.init(self.bytes) catch unreachable,
168 .remaining = self.count,
169 };
170 }
171
172 pub const Iterator = struct {
173 reader: Reader,
174 remaining: u32,
175
176 pub fn next(self: *Iterator) !?T {
177 if (self.remaining == 0) return null;
178 const result = try readEntry(T, &self.reader);
179 self.remaining -= 1;
180 return result;
181 }
182 };
183 };
184 }
185
186 pub fn decodeAddress(bytes: []const u8) !Address {
187 var reader = try Reader.init(bytes);
188 if (try reader.readInt(u32) != schema_version) return error.UnknownSchema;
189 var result: Address = undefined;
190 inline for (@typeInfo(Address).@"struct".field_names, 1..) |field, tag| {
191 @field(result, field) = try readField(&reader, tag);
192 }
193 if (result.producer.len == 0 or result.stage.len == 0) return error.InvalidAddress;
194 if (!reader.atEnd()) return error.InvalidRecord;
195 return result;
196 }
197
198 pub fn decodeInputs(bytes: []const u8) !InputView {
199 var reader = try Reader.init(bytes);
200 if (try reader.readInt(u32) != schema_version) return error.UnknownSchema;
201 const versions = try readEntries(Version, &reader, 2, "name");
202 const pipeline = try readEntries(Version, &reader, 3, null);
203 const options = try readField(&reader, 4);
204 const policy = try readField(&reader, 5);
205 const facts = try readEntries(Fact, &reader, 6, "key");
206 const dependencies = try readEntries(Dependency, &reader, 7, "role");
207 const gate_manifest = try readField(&reader, 8);
208 if (!reader.atEnd()) return error.InvalidRecord;
209 return .{
210 .versions = versions,
211 .pipeline = pipeline,
212 .options = options,
213 .policy = policy,
214 .facts = facts,
215 .dependencies = dependencies,
216 .gate_manifest = gate_manifest,
217 };
218 }
219
220 fn readEntries(
221 comptime T: type,
222 reader: *Reader,
223 tag: u8,
224 comptime ordered: ?[]const u8,
225 ) !Entries(T) {
226 if (try reader.readInt(u8) != tag) return error.UnknownRequiredField;
227 const count = try reader.readCount();
228 const start = reader.offset;
229 var previous: ?[]const u8 = null;
230 for (0..count) |_| {
231 const item = try readEntry(T, reader);
232 if (ordered) |field| try orderedKey(&previous, @field(item, field));
233 }
234 return .{ .bytes = reader.bytes[start..reader.offset], .count = @intCast(count) };
235 }
236
237 fn readEntry(comptime T: type, reader: *Reader) !T {
238 if (T == Version) {
239 const name = try reader.readString();
240 const version = try reader.readInt(u32);
241 if (version == 0 or name.len == 0) return error.InvalidVersion;
242 return .{ .name = name, .version = version };
243 } else if (T == Fact) {
244 return .{ .key = try reader.readString(), .value = try reader.readOptionalString() };
245 } else if (T == Dependency) {
246 const role = try reader.readString();
247 const exact = try reader.readBlob();
248 _ = try decodeExact(exact);
249 return .{ .role = role, .exact = exact };
250 } else @compileError("unknown canonical record collection");
251 }
252
253 pub const Namespace = enum(u8) { root, operation, region, block, value, attribute };
254 pub const EntityCounts = [@typeInfo(Namespace).@"enum".field_names.len]u32;
255
256 pub const Exact = struct {
257 address: []const u8,
258 inputs: []const u8,
259 image: []const u8,
260
261 pub fn eql(self: Exact, other: Exact) bool {
262 return simd.equal(Bytes, self.address, other.address) and
263 simd.equal(Bytes, self.inputs, other.inputs) and
264 simd.equal(Bytes, self.image, other.image);
265 }
266 };
267
268 const ValueTag = enum(u8) {
269 boolean = 1,
270 unsigned = 2,
271 signed = 3,
272 float_bits = 4,
273 enum_name = 5,
274 optional = 6,
275 sequence = 7,
276 structure = 8,
277 choice = 9,
278 bytes = 10,
279 unit = 11,
280 };
281
282 /// Keep each owner's projection exhaustive when its option or plan type changes.
283 pub fn requireFields(comptime T: type, comptime names: []const []const u8) void {
284 const fields = @typeInfo(T).@"struct".field_names;
285 if (fields.len != names.len) {
286 @compileError("classify every field before publishing this record");
287 }
288 inline for (fields, names) |field, expected| {
289 if (!std.mem.eql(u8, field, expected)) {
290 @compileError("update the explicit semantic projection");
291 }
292 }
293 }
294
295 /// Encode an owner's explicit semantic projection, never compiler pointers.
296 pub fn writeValue(writer: *Writer, value: anytype) ProjectionError!void {
297 try writeValueDepth(writer, value, 0);
298 }
299
300 fn writeValueDepth(writer: *Writer, value: anytype, depth: u8) ProjectionError!void {
301 if (depth >= maximum_projection_depth) return error.UnencodableProduct;
302 const T = @TypeOf(value);
303 switch (@typeInfo(T)) {
304 .void => try valueTag(writer, .unit),
305 .bool => {
306 try valueTag(writer, .boolean);
307 try writer.writeBool(value);
308 },
309 .int => |info| {
310 if (info.bits > 64) @compileError("declare a bounded wide-integer encoding");
311 try valueTag(writer, if (info.signedness == .signed) .signed else .unsigned);
312 if (info.signedness == .signed) try writer.writeInt(i64, value) else {
313 try writer.writeInt(u64, value);
314 }
315 },
316 .float => |info| {
317 try valueTag(writer, .float_bits);
318 try writer.writeInt(u16, info.bits);
319 const Bits = @Int(.unsigned, info.bits);
320 try writer.writeInt(Bits, @bitCast(value));
321 },
322 .@"enum" => {
323 try valueTag(writer, .enum_name);
324 try writer.writeString(@tagName(value));
325 },
326 .optional => {
327 try valueTag(writer, .optional);
328 try writer.writeBool(value != null);
329 if (value) |present| try writeValueDepth(writer, present, depth + 1);
330 },
331 .array, .pointer => try writeSequence(writer, value, depth),
332 .@"struct" => |info| {
333 try valueTag(writer, .structure);
334 try writer.writeCount(info.field_names.len);
335 inline for (info.field_names) |field| {
336 try writer.writeString(field);
337 try writeValueDepth(writer, @field(value, field), depth + 1);
338 }
339 },
340 .@"union" => {
341 try valueTag(writer, .choice);
342 try writer.writeString(@tagName(value));
343 switch (value) {
344 inline else => |payload| try writeValueDepth(writer, payload, depth + 1),
345 }
346 },
347 else => @compileError("classify this semantic field before publication"),
348 }
349 }
350
351 fn writeSequence(writer: *Writer, value: anytype, depth: u8) ProjectionError!void {
352 std.debug.assert(depth < maximum_projection_depth);
353 const info = @typeInfo(@TypeOf(value));
354 if (info == .pointer) {
355 if (info.pointer.size != .slice) {
356 @compileError("project references into exact revision ordinals");
357 }
358 if (info.pointer.child == u8) {
359 try valueTag(writer, .bytes);
360 try writer.writeBlob(value);
361 return;
362 }
363 }
364 try valueTag(writer, .sequence);
365 try writer.writeCount(value.len);
366 for (value) |item| try writeValueDepth(writer, item, depth + 1);
367 }
368
369 fn valueTag(writer: *Writer, value: ValueTag) !void {
370 try writer.writeTag(value);
371 }
372
373 pub fn encodeAddress(allocator: std.mem.Allocator, value: Address) ![]u8 {
374 if (value.producer.len == 0 or value.stage.len == 0) return error.InvalidAddress;
375 var writer = Writer.init(allocator);
376 defer writer.deinit();
377 var length: usize = 24;
378 inline for (@typeInfo(Address).@"struct".field_names) |field| {
379 length = try sizeAdd(length, @field(value, field).len);
380 }
381 try writer.bytes.ensureTotalCapacityPrecise(allocator, length);
382 try writer.writeInt(u32, schema_version);
383 inline for (@typeInfo(Address).@"struct".field_names, 1..) |field, tag| {
384 try writer.writeInt(u8, tag);
385 try writer.writeString(@field(value, field));
386 }
387 std.debug.assert(writer.bytes.items.len == length);
388 return writer.finish();
389 }
390
391 pub fn encodeInputs(
392 allocator: std.mem.Allocator,
393 inputs: Inputs,
394 dependencies: []const Dependency,
395 gate_manifest: []const u8,
396 ) ![]u8 {
397 comptime std.debug.assert(@typeInfo(Inputs).@"struct".field_names.len == 6);
398 const versions = try sortedMap(Version, allocator, inputs.versions, "name");
399 defer allocator.free(versions);
400 const facts = try sortedMap(Fact, allocator, inputs.facts, "key");
401 defer allocator.free(facts);
402 const ordered_dependencies = try sortedMap(Dependency, allocator, dependencies, "role");
403 defer allocator.free(ordered_dependencies);
404 var writer = Writer.init(allocator);
405 defer writer.deinit();
406 const length = try inputsSize(inputs, dependencies, gate_manifest);
407 try writer.bytes.ensureTotalCapacityPrecise(allocator, length);
408 try writer.writeInt(u32, schema_version);
409 try writer.writeInt(u8, 2);
410 try writeVersions(&writer, versions, true);
411 try writer.writeInt(u8, 3);
412 try writeVersions(&writer, inputs.pipeline, false);
413 try fieldBlob(&writer, 4, inputs.options);
414 try fieldBlob(&writer, 5, inputs.policy);
415 try writer.writeInt(u8, 6);
416 try writeFacts(&writer, facts);
417 try writer.writeInt(u8, 7);
418 try writeDependencies(&writer, ordered_dependencies);
419 try fieldBlob(&writer, 8, gate_manifest);
420 std.debug.assert(writer.bytes.items.len == length);
421 return writer.finish();
422 }
423
424 fn inputsSize(inputs: Inputs, dependencies: []const Dependency, gates: []const u8) !usize {
425 var size: usize = 39;
426 for ([_][]const u8{ inputs.options, inputs.policy, gates }) |bytes| {
427 size = try sizeAdd(size, bytes.len);
428 }
429 for ([_][]const Version{ inputs.versions, inputs.pipeline }) |versions| {
430 for (versions) |version| size = try sizeAdd(size, try sizeAdd(8, version.name.len));
431 }
432 for (inputs.facts) |fact| {
433 size = try sizeAdd(size, try sizeAdd(5, fact.key.len));
434 if (fact.value) |bytes| size = try sizeAdd(size, try sizeAdd(4, bytes.len));
435 }
436 for (dependencies) |dependency| {
437 size = try sizeAdd(size, try sizeAdd(8, dependency.role.len));
438 size = try sizeAdd(size, dependency.exact.len);
439 }
440 return size;
441 }
442
443 fn sizeAdd(first: usize, second: usize) !usize {
444 const size = std.math.add(usize, first, second) catch return error.RecordOverflow;
445 if (size > codec_limits.serialized_bytes) return error.RecordLimit;
446 return size;
447 }
448
449 fn sortedMap(
450 comptime T: type,
451 allocator: std.mem.Allocator,
452 source: []const T,
453 comptime key: []const u8,
454 ) ![]T {
455 const copy = try allocator.dupe(T, source);
456 errdefer allocator.free(copy);
457 std.mem.sort(T, copy, {}, struct {
458 fn less(_: void, first: T, second: T) bool {
459 return std.mem.lessThan(u8, @field(first, key), @field(second, key));
460 }
461 }.less);
462 var previous: ?[]const u8 = null;
463 for (copy) |item| try orderedKey(&previous, @field(item, key));
464 return copy;
465 }
466
467 fn fieldBlob(writer: anytype, tag: u8, bytes: []const u8) !void {
468 try writer.writeInt(u8, tag);
469 try writer.writeBlob(bytes);
470 }
471
472 fn writeVersions(writer: *Writer, versions: []const Version, sorted: bool) !void {
473 try writer.writeCount(versions.len);
474 var previous: ?[]const u8 = null;
475 for (versions) |version| {
476 if (version.version == 0 or version.name.len == 0) return error.InvalidVersion;
477 if (sorted) try orderedKey(&previous, version.name);
478 try writer.writeString(version.name);
479 try writer.writeInt(u32, version.version);
480 }
481 }
482
483 fn writeFacts(writer: *Writer, facts: []const Fact) !void {
484 try writer.writeCount(facts.len);
485 var previous: ?[]const u8 = null;
486 for (facts) |fact| {
487 try orderedKey(&previous, fact.key);
488 try writer.writeString(fact.key);
489 try writer.writeOptionalString(fact.value);
490 }
491 }
492
493 fn writeDependencies(writer: *Writer, dependencies: []const Dependency) !void {
494 try writer.writeCount(dependencies.len);
495 var previous: ?[]const u8 = null;
496 for (dependencies) |dependency| {
497 try orderedKey(&previous, dependency.role);
498 _ = try decodeExact(dependency.exact);
499 try writer.writeString(dependency.role);
500 try writer.writeBlob(dependency.exact);
501 }
502 }
503
504 fn orderedKey(previous: *?[]const u8, key: []const u8) !void {
505 if (key.len == 0) return error.InvalidKey;
506 if (previous.*) |prior| {
507 if (!std.mem.lessThan(u8, prior, key)) return error.NoncanonicalOrder;
508 }
509 previous.* = key;
510 }
511
512 pub fn encodeExact(allocator: std.mem.Allocator, exact: Exact) ![]u8 {
513 var writer = Writer.init(allocator);
514 defer writer.deinit();
515 try writeExact(&writer, exact);
516 return writer.finish();
517 }
518
519 /// The caller retains ownership of the output storage. Preflight refuses a short
520 /// buffer before writing; the writer has no allocator capacity for growth.
521 pub fn encodeExactInto(output: []u8, exact: Exact) ![]u8 {
522 const length = try exactSize(exact.address.len, exact.inputs.len, exact.image.len);
523 if (length > output.len) return error.RecordLimit;
524 var writer = binary.FixedWriter(codec_limits).init(output);
525 defer writer.deinit();
526 try writeExact(&writer, exact);
527 std.debug.assert(writer.bytes.items.len == length);
528 std.debug.assert(writer.bytes.items.ptr == output.ptr);
529 return writer.finish();
530 }
531
532 pub fn exactSize(address: usize, inputs: usize, image: usize) !usize {
533 const prefix = std.math.add(usize, address, inputs) catch return error.RecordOverflow;
534 const payload = std.math.add(usize, prefix, image) catch return error.RecordOverflow;
535 const length = std.math.add(usize, payload, 19) catch return error.RecordOverflow;
536 if (address > std.math.maxInt(u32) or inputs > std.math.maxInt(u32) or
537 image > std.math.maxInt(u32)) return error.RecordLimit;
538 return length;
539 }
540
541 fn writeExact(writer: anytype, exact: Exact) !void {
542 comptime requireFields(Exact, &.{ "address", "inputs", "image" });
543 try writer.writeInt(u32, schema_version);
544 try fieldBlob(writer, 1, exact.address);
545 try fieldBlob(writer, 2, exact.inputs);
546 try fieldBlob(writer, 3, exact.image);
547 }
548
549 pub fn decodeExact(bytes: []const u8) !Exact {
550 var reader = try Reader.init(bytes);
551 if (try reader.readInt(u32) != schema_version) return error.UnknownSchema;
552 const address = try readField(&reader, 1);
553 const inputs = try readField(&reader, 2);
554 const image = try readField(&reader, 3);
555 if (!reader.atEnd()) return error.InvalidRecord;
556 return .{ .address = address, .inputs = inputs, .image = image };
557 }
558
559 fn readField(reader: *Reader, tag: u8) ![]const u8 {
560 if (try reader.readInt(u8) != tag) return error.UnknownRequiredField;
561 return reader.readBlob();
562 }
563
564 pub fn validateObservations(declared: []const Fact, observed: []const Fact) !void {
565 if (declared.len != observed.len) return error.IncompleteObservations;
566 for (declared) |requested| {
567 for (observed) |actual| {
568 if (requested.eql(actual)) break;
569 } else return error.UndeclaredObservation;
570 }
571 }
572
573 test "revision records preserve negative reads, pipeline order and exact bytes" {
574 const allocator = std.testing.allocator;
575 const versions = [_]Version{.{ .name = "compiler", .version = 1 }};
576 const pipeline = [_]Version{
577 .{ .name = "b", .version = 2 },
578 .{ .name = "a", .version = 1 },
579 };
580 const inputs = Inputs{
581 .compiler_manifest = "owned build inputs",
582 .versions = &versions,
583 .pipeline = &pipeline,
584 .options = "expanded defaults",
585 .policy = "strict",
586 .facts = &.{.{ .key = "absent", .value = null }},
587 };
588 const first = try encodeInputs(allocator, inputs, &.{}, "gates");
589 defer allocator.free(first);
590 var changed = inputs;
591 changed.facts = &.{.{ .key = "absent", .value = "" }};
592 const second = try encodeInputs(allocator, changed, &.{}, "gates");
593 defer allocator.free(second);
594 try std.testing.expect(!std.mem.eql(u8, first, second));
595 changed = inputs;
596 const reversed = [_]Version{ pipeline[1], pipeline[0] };
597 changed.pipeline = &reversed;
598 const third = try encodeInputs(allocator, changed, &.{}, "gates");
599 defer allocator.free(third);
600 try std.testing.expect(!std.mem.eql(u8, first, third));
601 const exact = Exact{ .address = "address", .inputs = first, .image = "image" };
602 const bytes = try encodeExact(allocator, exact);
603 defer allocator.free(bytes);
604 try std.testing.expect(exact.eql(try decodeExact(bytes)));
605 bytes[4] = 99;
606 try std.testing.expectError(error.UnknownRequiredField, decodeExact(bytes));
607 }
608
609 test "revision records refuse undeclared observations and noncanonical maps" {
610 try std.testing.expectError(error.UndeclaredObservation, validateObservations(
611 &.{.{ .key = "capability", .value = null }},
612 &.{.{ .key = "capability", .value = "present" }},
613 ));
614 var writer = Writer.init(std.testing.allocator);
615 defer writer.deinit();
616 try std.testing.expectError(error.NoncanonicalOrder, writeFacts(&writer, &.{
617 .{ .key = "z", .value = null },
618 .{ .key = "a", .value = "value" },
619 }));
620 }
621
622 test "revision records sort map keys while observations retain negative values" {
623 const allocator = std.testing.allocator;
624 const facts = [_]Fact{
625 .{ .key = "z", .value = null },
626 .{ .key = "a", .value = "" },
627 };
628 const versions = [_]Version{
629 .{ .name = "z", .version = 2 },
630 .{ .name = "a", .version = 1 },
631 };
632 var inputs = Inputs{
633 .compiler_manifest = "compiled inputs",
634 .versions = &versions,
635 .pipeline = &.{},
636 .options = "defaults",
637 .policy = "strict",
638 .facts = &facts,
639 };
640 const first = try encodeInputs(allocator, inputs, &.{}, "gates");
641 defer allocator.free(first);
642 const reversed_facts = [_]Fact{ facts[1], facts[0] };
643 const reversed_versions = [_]Version{ versions[1], versions[0] };
644 inputs.facts = &reversed_facts;
645 inputs.versions = &reversed_versions;
646 const second = try encodeInputs(allocator, inputs, &.{}, "gates");
647 defer allocator.free(second);
648 try std.testing.expectEqualSlices(u8, first, second);
649 try validateObservations(&facts, &reversed_facts);
650 try std.testing.expectError(error.UndeclaredObservation, validateObservations(
651 &facts,
652 &.{ facts[0], facts[0] },
653 ));
654 inputs.facts = &.{ facts[0], facts[0] };
655 try std.testing.expectError(error.NoncanonicalOrder, encodeInputs(
656 allocator,
657 inputs,
658 &.{},
659 "gates",
660 ));
661 }
662
663 test "revision records semantic projections preserve float bits and tagged choices" {
664 const Choice = union(enum) { absent, threshold: u32 };
665 const Options = struct { value: f64, choice: Choice, sequence: [2]u16, note: ?[]const u8 };
666 var first = Writer.init(std.testing.allocator);
667 defer first.deinit();
668 var second = Writer.init(std.testing.allocator);
669 defer second.deinit();
670 var options = Options{
671 .value = @bitCast(@as(u64, 0x7ff8000000000042)),
672 .choice = .{ .threshold = 11 },
673 .sequence = .{ 2, 1 },
674 .note = null,
675 };
676 try writeValue(&first, options);
677 try writeValue(&second, options);
678 try std.testing.expectEqualSlices(u8, first.bytes.items, second.bytes.items);
679 second.bytes.clearRetainingCapacity();
680 options.value = @bitCast(@as(u64, 0x7ff8000000000043));
681 try writeValue(&second, options);
682 try std.testing.expect(!std.mem.eql(u8, first.bytes.items, second.bytes.items));
683 second.bytes.clearRetainingCapacity();
684 options.value = @bitCast(@as(u64, 0x7ff8000000000042));
685 options.choice = .absent;
686 try writeValue(&second, options);
687 try std.testing.expect(!std.mem.eql(u8, first.bytes.items, second.bytes.items));
688 }
689
690 test "revision records expose exact address and input closure without mutable owners" {
691 const allocator = std.testing.allocator;
692 const address = Address{
693 .producer = "compiler",
694 .source = "entry",
695 .stage = "ir",
696 .variant = "a",
697 };
698 const encoded_address = try encodeAddress(allocator, address);
699 defer allocator.free(encoded_address);
700 try std.testing.expect(address.eql(try decodeAddress(encoded_address)));
701 const dependency = try encodeExact(allocator, .{
702 .address = encoded_address,
703 .inputs = "input",
704 .image = "image",
705 });
706 defer allocator.free(dependency);
707 const inputs = try encodeInputs(allocator, .{
708 .compiler_manifest = "compiled inputs",
709 .versions = &.{},
710 .pipeline = &.{.{ .name = "pass", .version = 2 }},
711 .options = "defaults",
712 .policy = "strict",
713 .facts = &.{.{ .key = "absent", .value = null }},
714 }, &.{.{ .role = "source", .exact = dependency }}, "gates");
715 defer allocator.free(inputs);
716 const view = try decodeInputs(inputs);
717 var pipeline = view.pipeline.iterator();
718 try std.testing.expectEqualStrings("pass", (try pipeline.next()).?.name);
719 try std.testing.expectEqual(null, try pipeline.next());
720 var facts = view.facts.iterator();
721 try std.testing.expectEqual(null, (try facts.next()).?.value);
722 var dependencies = view.dependencies.iterator();
723 try std.testing.expectEqualSlices(u8, dependency, (try dependencies.next()).?.exact);
724 try std.testing.expectEqual(null, try dependencies.next());
725 }
726
727 test "revision records encode into reserved capacity without changing exact bytes" {
728 const exact = Exact{
729 .address = "address",
730 .inputs = "exact inputs",
731 .image = "canonical image",
732 };
733 const expected = try encodeExact(std.testing.allocator, exact);
734 defer std.testing.allocator.free(expected);
735 var storage: [256]u8 = undefined;
736 const actual = try encodeExactInto(storage[0..expected.len], exact);
737 try std.testing.expect(@intFromPtr(actual.ptr) == @intFromPtr(&storage));
738 try std.testing.expectEqualSlices(u8, expected, actual);
739 try std.testing.expectEqual(expected.len, try exactSize(
740 exact.address.len,
741 exact.inputs.len,
742 exact.image.len,
743 ));
744 @memset(&storage, 0xa5);
745 try std.testing.expectError(error.RecordLimit, encodeExactInto(
746 storage[0 .. expected.len - 1],
747 exact,
748 ));
749 for (storage) |byte| try std.testing.expectEqual(@as(u8, 0xa5), byte);
750 try std.testing.expectError(error.RecordOverflow, exactSize(std.math.maxInt(usize), 1, 0));
751 }
752
753 test "revision records bound semantic projection depth at the exact limit" {
754 const AtLimit = comptime blk: {
755 var T: type = u16;
756 for (1..maximum_projection_depth) |_| T = [1]T;
757 break :blk T;
758 };
759 var writer = Writer.init(std.testing.allocator);
760 defer writer.deinit();
761 try writeValue(&writer, std.mem.zeroes(AtLimit));
762 try std.testing.expect(writer.bytes.items.len > 0);
763 writer.bytes.clearRetainingCapacity();
764 try std.testing.expectError(error.UnencodableProduct, writeValue(
765 &writer,
766 std.mem.zeroes([1]AtLimit),
767 ));
768 }
769
770 test "revision records reject cyclic semantic projections within bounded depth" {
771 const Node = struct { children: []const @This() };
772 var nodes: [1]Node = undefined;
773 nodes[0] = .{ .children = &nodes };
774 var writer = Writer.init(std.testing.allocator);
775 defer writer.deinit();
776 try std.testing.expectError(error.UnencodableProduct, writeValue(&writer, nodes[0]));
777 }