lib/choir/src/bytecode/qualification.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const ir = @import("../core/root.zig");
  3 const bytecode = @import("root.zig");
  4 
  5 pub const Limits = struct {
  6     operations: u32,
  7     entities: u32,
  8     fields: u32,
  9     depth: u16,
 10 };
 11 
 12 /// Encode with the bytecode owner, then compare every supported semantic field.
 13 /// The caller owns and configures an independent decoding Context.
 14 pub fn encode(
 15     allocator: std.mem.Allocator,
 16     source: *ir.Operation,
 17     resources: []const bytecode.Resource,
 18     decode_context: *ir.Context,
 19     limits: Limits,
 20 ) ![]u8 {
 21     if (source.context == decode_context) return error.IsolatedDecodeRequired;
 22     const ordered = try sortedResources(allocator, resources, limits);
 23     defer allocator.free(ordered);
 24     try compare(allocator, source, source, ordered, ordered, limits);
 25     const bytes = bytecode.encodeModuleWithResources(allocator, source, ordered) catch |err| {
 26         return encodingError(err);
 27     };
 28     errdefer allocator.free(bytes);
 29     var decoded = bytecode.decodeModule(allocator, decode_context, bytes) catch |err| {
 30         return encodingError(err);
 31     };
 32     defer decoded.deinit();
 33     try compare(allocator, source, decoded.module, ordered, decoded.resources, limits);
 34     return bytes;
 35 }
 36 
 37 fn sortedResources(
 38     allocator: std.mem.Allocator,
 39     resources: []const bytecode.Resource,
 40     limits: Limits,
 41 ) ![]bytecode.Resource {
 42     if (resources.len > limits.fields) return error.UnencodableProduct;
 43     const copy = try allocator.dupe(bytecode.Resource, resources);
 44     errdefer allocator.free(copy);
 45     std.mem.sort(bytecode.Resource, copy, {}, resourceLess);
 46     for (copy, 0..) |item, index| {
 47         if (index == 0) continue;
 48         if (!resourceLess({}, copy[index - 1], item)) return error.UnencodableProduct;
 49     }
 50     return copy;
 51 }
 52 
 53 fn resourceLess(_: void, first: bytecode.Resource, second: bytecode.Resource) bool {
 54     const namespace = std.mem.order(u8, first.namespace, second.namespace);
 55     if (namespace != .eq) return namespace == .lt;
 56     return std.mem.lessThan(u8, first.name, second.name);
 57 }
 58 
 59 fn encodingError(err: anyerror) anyerror {
 60     return if (err == error.OutOfMemory) err else error.UnencodableProduct;
 61 }
 62 
 63 pub fn compare(
 64     allocator: std.mem.Allocator,
 65     source: *ir.Operation,
 66     decoded: *ir.Operation,
 67     source_resources: []const bytecode.Resource,
 68     decoded_resources: []const bytecode.Resource,
 69     limits: Limits,
 70 ) !void {
 71     var before = try Tree.collect(allocator, source, limits);
 72     defer before.deinit();
 73     var after = try Tree.collect(allocator, decoded, limits);
 74     defer after.deinit();
 75     if (before.operations.items.len != after.operations.items.len or
 76         before.blocks.items.len != after.blocks.items.len or
 77         before.values.items.len != after.values.items.len) return error.UnencodableProduct;
 78     var fields = Fields{ .allocator = allocator, .limits = limits };
 79     defer fields.tasks.deinit(allocator);
 80     for (before.operations.items, after.operations.items) |first, second| {
 81         try compareOperation(&fields, &before, &after, first, second);
 82     }
 83     for (before.blocks.items, after.blocks.items) |first, second| {
 84         try compareBlock(&fields, first, second);
 85     }
 86     try fields.drain();
 87     try compareResources(source_resources, decoded_resources);
 88 }
 89 
 90 const Tree = struct {
 91     allocator: std.mem.Allocator,
 92     limits: Limits,
 93     root: *ir.Operation,
 94     operations: std.ArrayListUnmanaged(*ir.Operation) = .empty,
 95     blocks: std.ArrayListUnmanaged(*ir.Block) = .empty,
 96     values: std.ArrayListUnmanaged(*ir.Value) = .empty,
 97 
 98     fn collect(allocator: std.mem.Allocator, root: *ir.Operation, limits: Limits) !Tree {
 99         var result = Tree{ .allocator = allocator, .limits = limits, .root = root };
100         errdefer result.deinit();
101         _ = try root.walk(.{ .order = .pre_order }, &result, visit);
102         return result;
103     }
104 
105     fn deinit(self: *Tree) void {
106         self.operations.deinit(self.allocator);
107         self.blocks.deinit(self.allocator);
108         self.values.deinit(self.allocator);
109     }
110 
111     fn visit(self: *Tree, op: *ir.Operation) !ir.WalkResult {
112         if (self.operations.items.len == self.limits.operations) return error.UnencodableProduct;
113         try self.validateDepth(op);
114         try validateOperationStorage(op);
115         try self.operations.append(self.allocator, op);
116         for (op.results.items) |*value| try self.addValue(value);
117         for (op.regions.items) |*region| {
118             var count: usize = 0;
119             var blocks = region.getBlocks();
120             while (blocks.next()) |block| {
121                 if (self.blocks.items.len == self.limits.entities) return error.UnencodableProduct;
122                 try self.blocks.append(self.allocator, block);
123                 try validateBlockStorage(block);
124                 for (block.arguments.items) |value| try self.addValue(value);
125                 count += 1;
126             }
127             if (count != region.blocks.size) return error.UnencodableProduct;
128         }
129         return .advance;
130     }
131 
132     fn validateDepth(self: *const Tree, op: *ir.Operation) !void {
133         var current = op;
134         var depth: usize = 0;
135         while (current != self.root) : (depth += 1) {
136             if (depth == self.limits.depth) return error.UnencodableProduct;
137             current = current.getParentOp() orelse return error.UnencodableProduct;
138         }
139     }
140 
141     fn addValue(self: *Tree, value: *ir.Value) !void {
142         if (self.values.items.len == self.limits.entities) return error.UnencodableProduct;
143         try self.values.append(self.allocator, value);
144     }
145 
146     fn valueOrdinal(self: *const Tree, value: *const ir.Value) !usize {
147         for (self.values.items, 0..) |candidate, ordinal| {
148             if (candidate == value) return ordinal;
149         }
150         return error.UnboundProductInput;
151     }
152 
153     fn blockOrdinal(self: *const Tree, block: ?*const ir.Block) !?usize {
154         const expected = block orelse return null;
155         for (self.blocks.items, 0..) |candidate, ordinal| {
156             if (candidate == expected) return ordinal;
157         }
158         return error.UnboundProductInput;
159     }
160 };
161 
162 fn validateOperationStorage(op: *ir.Operation) !void {
163     if (op.operand_values.len != op.operands.items.len or
164         op.result_types.len != op.results.items.len) return error.UnencodableProduct;
165     for (op.operands.items, op.operand_values, 0..) |operand, value, index| {
166         if (operand.value != value or operand.operand_number != index or
167             operand.owner != @as(*anyopaque, @ptrCast(op))) return error.UnencodableProduct;
168         if (operand.operand_value_slot != &op.operand_values[index]) {
169             return error.UnencodableProduct;
170         }
171     }
172     for (op.results.items, op.result_types, 0..) |value, typ, index| {
173         if (!value.type.eql(typ) or value.kind != .op_result) return error.UnencodableProduct;
174         const info = value.kind.op_result;
175         if (info.result_number != index or info.owner != @as(*anyopaque, @ptrCast(op))) {
176             return error.UnencodableProduct;
177         }
178     }
179 }
180 
181 fn validateBlockStorage(block: *ir.Block) !void {
182     if (block.arguments.items.len != block.argument_locations.items.len) {
183         return error.UnencodableProduct;
184     }
185     for (block.arguments.items, 0..) |argument, index| {
186         if (argument.kind != .block_argument) return error.UnencodableProduct;
187         const info = argument.kind.block_argument;
188         if (info.arg_number != index or info.owner != @as(*anyopaque, @ptrCast(block))) {
189             return error.UnencodableProduct;
190         }
191     }
192 }
193 
194 fn compareOperation(
195     fields: *Fields,
196     before: *const Tree,
197     after: *const Tree,
198     first: *ir.Operation,
199     second: *ir.Operation,
200 ) !void {
201     try equalBytes(first.name.name, second.name.name);
202     if (first.operands.items.len != second.operands.items.len or
203         first.results.items.len != second.results.items.len or
204         first.regions.items.len != second.regions.items.len or
205         first.successors.items.len != second.successors.items.len) return error.UnencodableProduct;
206     try fields.push(.{ .location = .{ .first = first.location, .second = second.location } });
207     for (first.results.items, second.results.items) |a, b| try equalType(a.type, b.type);
208     for (first.operands.items, second.operands.items) |a, b| {
209         if (try before.valueOrdinal(a.value) != try after.valueOrdinal(b.value)) {
210             return error.UnencodableProduct;
211         }
212         try equalType(a.value.type, b.value.type);
213     }
214     for (first.successors.items, second.successors.items) |a, b| {
215         if (try before.blockOrdinal(a) != try after.blockOrdinal(b)) {
216             return error.UnencodableProduct;
217         }
218     }
219     if (first != before.operations.items[0]) {
220         const source_parent = try before.blockOrdinal(first.parent_block);
221         const decoded_parent = try after.blockOrdinal(second.parent_block);
222         if (source_parent != decoded_parent) return error.UnencodableProduct;
223     }
224     for (first.regions.items, second.regions.items) |*a, *b| {
225         if (a.blocks.size != b.blocks.size) return error.UnencodableProduct;
226         if (try before.blockOrdinal(a.blocks.head) != try after.blockOrdinal(b.blocks.head)) {
227             return error.UnencodableProduct;
228         }
229     }
230     try compareDictionary(fields, first.getRawDictionaryAttrs(), second.getRawDictionaryAttrs());
231     try compareProperties(fields, first, second);
232 }
233 
234 fn compareBlock(fields: *Fields, first: *ir.Block, second: *ir.Block) !void {
235     if (first.arguments.items.len != second.arguments.items.len) return error.UnencodableProduct;
236     if (first.argument_locations.items.len != first.arguments.items.len or
237         second.argument_locations.items.len != second.arguments.items.len)
238     {
239         return error.UnencodableProduct;
240     }
241     for (first.arguments.items, second.arguments.items, 0..) |a, b, index| {
242         try equalType(a.type, b.type);
243         try fields.push(.{ .location = .{
244             .first = first.argument_locations.items[index],
245             .second = second.argument_locations.items[index],
246         } });
247     }
248 }
249 
250 fn compareDictionary(
251     fields: *Fields,
252     first: []const ir.NamedAttribute,
253     second: []const ir.NamedAttribute,
254 ) !void {
255     if (first.len != second.len) return error.UnencodableProduct;
256     for (first, second) |a, b| {
257         try equalBytes(a.name, b.name);
258         try fields.push(.{ .attribute = .{ .first = a.value, .second = b.value } });
259     }
260 }
261 
262 fn compareProperties(fields: *Fields, first: *ir.Operation, second: *ir.Operation) !void {
263     const model = first.properties.model orelse {
264         if (second.properties.model != null) return error.UnencodableProduct;
265         return;
266     };
267     const decoded_model = second.properties.model orelse return error.UnencodableProduct;
268     if (model.serialization != .single_attribute or
269         decoded_model.serialization != .single_attribute) return error.UnencodableProduct;
270     try equalBytes(model.name, decoded_model.name);
271     const original = (first.getPropertiesAsAttr() catch return error.UnencodableProduct) orelse {
272         if (try second.getPropertiesAsAttr() != null) return error.UnencodableProduct;
273         return;
274     };
275     const decoded = (second.getPropertiesAsAttr() catch return error.UnencodableProduct) orelse {
276         return error.UnencodableProduct;
277     };
278     try fields.push(.{ .attribute = .{ .first = original, .second = decoded } });
279 }
280 
281 fn equalBytes(first: []const u8, second: []const u8) !void {
282     if (!std.mem.eql(u8, first, second)) return error.UnencodableProduct;
283 }
284 
285 fn equalType(first: ir.Type, second: ir.Type) !void {
286     const a = first.getDialectStorage() orelse return error.UnencodableProduct;
287     const b = second.getDialectStorage() orelse return error.UnencodableProduct;
288     comptime std.debug.assert(@typeInfo(ir.Type.DialectTypeStorage).@"struct".field_names.len == 5);
289     try equalBytes(a.name, b.name);
290     try equalBytes(a.param_key, b.param_key);
291 }
292 
293 fn compareResources(first: []const bytecode.Resource, second: []const bytecode.Resource) !void {
294     if (first.len != second.len) return error.UnencodableProduct;
295     for (first, second) |a, b| {
296         inline for (@typeInfo(bytecode.Resource).@"struct".field_names) |field| {
297             try equalBytes(@field(a, field), @field(b, field));
298         }
299     }
300 }
301 
302 fn Pair(comptime T: type) type {
303     return struct { first: T, second: T, depth: u16 = 0 };
304 }
305 
306 const Task = union(enum) {
307     attribute: Pair(ir.Attribute),
308     location: Pair(ir.Location),
309 };
310 
311 const Fields = struct {
312     allocator: std.mem.Allocator,
313     limits: Limits,
314     tasks: std.ArrayListUnmanaged(Task) = .empty,
315 
316     fn push(self: *Fields, task: Task) !void {
317         const depth = switch (task) {
318             inline else => |pair| pair.depth,
319         };
320         if (self.tasks.items.len == self.limits.fields or depth > self.limits.depth) {
321             return error.UnencodableProduct;
322         }
323         try self.tasks.append(self.allocator, task);
324     }
325 
326     fn drain(self: *Fields) !void {
327         var index: usize = 0;
328         while (index < self.tasks.items.len) : (index += 1) {
329             switch (self.tasks.items[index]) {
330                 .attribute => |pair| try self.attribute(pair),
331                 .location => |pair| try self.location(pair),
332             }
333         }
334     }
335 
336     fn attribute(self: *Fields, pair: Pair(ir.Attribute)) !void {
337         const first = pair.first;
338         const second = pair.second;
339         try equalBytes(first.abstract.name, second.abstract.name);
340         inline for (.{
341             ir.Attribute.IntegerAttr,  ir.Attribute.FloatAttr,     ir.Attribute.BoolAttr,
342             ir.Attribute.StringAttr,   ir.Attribute.SymbolRefAttr, ir.Attribute.StringListAttr,
343             ir.Attribute.TypeListAttr, ir.Attribute.ArrayAttr,
344         }) |T| {
345             if (first.cast(T)) |a| {
346                 const b = second.cast(T) orelse return error.UnencodableProduct;
347                 try self.attributeFields(T, a, b, pair.depth);
348                 return;
349             }
350         }
351         const a = first.cast(ir.Attribute.DialectAttr) orelse return error.UnencodableProduct;
352         const b = second.cast(ir.Attribute.DialectAttr) orelse return error.UnencodableProduct;
353         try self.attributeFields(ir.Attribute.DialectAttr, a, b, pair.depth);
354     }
355 
356     fn attributeFields(
357         self: *Fields,
358         comptime T: type,
359         first: *const T,
360         second: *const T,
361         depth: u16,
362     ) !void {
363         inline for (@typeInfo(T).@"struct".field_names) |field| {
364             if (comptime std.mem.eql(u8, field, "context")) continue;
365             const a = @field(first, field);
366             const b = @field(second, field);
367             const Field = @TypeOf(a);
368             if (Field == []const u8) {
369                 try equalBytes(a, b);
370             } else if (Field == f64) {
371                 if (@as(u64, @bitCast(a)) != @as(u64, @bitCast(b))) return error.UnencodableProduct;
372             } else if (Field == []const ir.Attribute or Field == []const ir.Type or
373                 Field == []const []const u8)
374             {
375                 if (a.len != b.len) return error.UnencodableProduct;
376                 for (a, b) |x, y| {
377                     if (Field == []const ir.Attribute) {
378                         if (depth == std.math.maxInt(u16)) return error.UnencodableProduct;
379                         try self.push(.{ .attribute = .{
380                             .first = x,
381                             .second = y,
382                             .depth = depth + 1,
383                         } });
384                     } else if (Field == []const ir.Type) {
385                         try equalType(x, y);
386                     } else try equalBytes(x, y);
387                 }
388             } else {
389                 switch (@typeInfo(Field)) {
390                     .int, .bool => if (a != b) return error.UnencodableProduct,
391                     else => @compileError("classify the new attribute field for complete capture"),
392                 }
393             }
394         }
395     }
396 
397     fn location(self: *Fields, pair: Pair(ir.Location)) !void {
398         const a = pair.first;
399         const b = pair.second;
400         if (std.meta.activeTag(a) != std.meta.activeTag(b)) return error.UnencodableProduct;
401         switch (a) {
402             .unknown => {},
403             .file => |file| {
404                 try equalBytes(file.filename, b.file.filename);
405                 if (file.line != b.file.line or file.column != b.file.column) {
406                     return error.UnencodableProduct;
407                 }
408             },
409             .file_range => |range| {
410                 try equalBytes(range.filename, b.file_range.filename);
411                 if (!std.meta.eql(range.start, b.file_range.start) or
412                     !std.meta.eql(range.end, b.file_range.end)) return error.UnencodableProduct;
413             },
414             .name => |name| {
415                 try equalBytes(name.name, b.name.name);
416                 if (name.child) |child| {
417                     const other = b.name.child orelse return error.UnencodableProduct;
418                     try self.childLocation(child.*, other.*, pair.depth);
419                 } else if (b.name.child != null) return error.UnencodableProduct;
420             },
421             .fused => |fused| {
422                 if (fused.metadata != null or b.fused.metadata != null or
423                     fused.locations.len != b.fused.locations.len) return error.UnencodableProduct;
424                 for (fused.locations, b.fused.locations) |x, y| {
425                     try self.childLocation(x, y, pair.depth);
426                 }
427             },
428             .call_site => |site| {
429                 try self.childLocation(site.callee.*, b.call_site.callee.*, pair.depth);
430                 try self.childLocation(site.caller.*, b.call_site.caller.*, pair.depth);
431             },
432         }
433     }
434 
435     fn childLocation(self: *Fields, first: ir.Location, second: ir.Location, depth: u16) !void {
436         if (depth == std.math.maxInt(u16)) return error.UnencodableProduct;
437         try self.push(.{ .location = .{ .first = first, .second = second, .depth = depth + 1 } });
438     }
439 };
440 
441 const test_limits = Limits{ .operations = 100, .entities = 100, .fields = 1000, .depth = 32 };
442 
443 fn testContext(allocator: std.mem.Allocator) !ir.Context {
444     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
445     errdefer ctx.deinit(allocator);
446     try ctx.allowUnregistered();
447     return ctx;
448 }
449 
450 fn testModule(ctx: *ir.Context) !*ir.Operation {
451     var state = ir.Operation.State.init("test.module", .getFile("source", 2, 3));
452     state.addRegion();
453     const module = try ctx.createOperation(state);
454     const block = try module.getRegion(0).?.addBlock();
455     const typ = try ctx.getDialectTypeFromNameWithKey("test.word", "32");
456     const argument = try block.addArgument(typ, .getFile("argument", 4, 5));
457     const float = try ctx.getF64Attr(@bitCast(@as(u64, 0x7ff8000000000042)));
458     const text = try ctx.getStringAttr("owned bytes");
459     const array = try ctx.getArrayAttr(&.{ float, text });
460     var operation = ir.Operation.State.init("test.use", .getFile("use", 7, 8));
461     operation.addOperands(&.{argument});
462     operation.addTypes(&.{typ});
463     operation.addAttributes(&.{.{ .name = "payload", .value = array }});
464     const child = try ctx.createOperation(operation);
465     try block.addOperation(child);
466     return module;
467 }
468 
469 test "bytecode qualification compares block argument locations and resource bytes" {
470     const allocator = std.testing.allocator;
471     var source_ctx = try testContext(allocator);
472     defer source_ctx.deinit(allocator);
473     var decode_ctx = try testContext(allocator);
474     defer decode_ctx.deinit(allocator);
475     const source = try testModule(&source_ctx);
476     const resources = [_]bytecode.Resource{.{
477         .namespace = "test",
478         .name = "resource",
479         .type_id = "bytes/v1",
480         .data = "original",
481     }};
482     const bytes = try encode(allocator, source, &resources, &decode_ctx, test_limits);
483     defer allocator.free(bytes);
484     var decoded = try bytecode.decodeModule(allocator, &decode_ctx, bytes);
485     defer decoded.deinit();
486     try compare(allocator, source, decoded.module, &resources, decoded.resources, test_limits);
487     const block = decoded.module.getRegion(0).?.getEntryBlock().?;
488     const previous = block.getArgumentLocation(0).?;
489     block.setArgumentLocation(0, .getFile("changed", 1, 1));
490     try std.testing.expectError(error.UnencodableProduct, compare(
491         allocator,
492         source,
493         decoded.module,
494         &resources,
495         decoded.resources,
496         test_limits,
497     ));
498     block.setArgumentLocation(0, previous);
499     var changed = resources;
500     changed[0].data = "changed";
501     try std.testing.expectError(error.UnencodableProduct, compare(
502         allocator,
503         source,
504         decoded.module,
505         &changed,
506         decoded.resources,
507         test_limits,
508     ));
509 }
510 
511 test "bytecode qualification refuses fused metadata that ordinary round trips omit" {
512     const allocator = std.testing.allocator;
513     var source_ctx = try testContext(allocator);
514     defer source_ctx.deinit(allocator);
515     var decode_ctx = try testContext(allocator);
516     defer decode_ctx.deinit(allocator);
517     const source = try testModule(&source_ctx);
518     const metadata: u32 = 42;
519     source.location = .{ .fused = .{ .locations = &.{.unknown}, .metadata = &metadata } };
520     const bytes = try bytecode.encodeModule(allocator, source);
521     defer allocator.free(bytes);
522     var decoded = try bytecode.decodeModule(allocator, &decode_ctx, bytes);
523     defer decoded.deinit();
524     try std.testing.expectEqual(null, decoded.module.location.fused.metadata);
525     try std.testing.expectError(error.UnencodableProduct, encode(
526         allocator,
527         source,
528         &.{},
529         &decode_ctx,
530         test_limits,
531     ));
532 }
533 
534 test "bytecode qualification refuses undeclared free values and shared decoding contexts" {
535     const allocator = std.testing.allocator;
536     var source_ctx = try testContext(allocator);
537     defer source_ctx.deinit(allocator);
538     var decode_ctx = try testContext(allocator);
539     defer decode_ctx.deinit(allocator);
540     const source = try testModule(&source_ctx);
541     try std.testing.expectError(error.IsolatedDecodeRequired, encode(
542         allocator,
543         source,
544         &.{},
545         &source_ctx,
546         test_limits,
547     ));
548     var operations = source.getRegion(0).?.getEntryBlock().?.getOperations();
549     const child = operations.next().?;
550     try std.testing.expectError(error.UnboundProductInput, encode(
551         allocator,
552         child,
553         &.{},
554         &decode_ctx,
555         test_limits,
556     ));
557 }
558 
559 fn classify(
560     comptime T: type,
561     comptime semantic: []const []const u8,
562     comptime derived: []const []const u8,
563     comptime process: []const []const u8,
564 ) void {
565     comptime {
566         const fields = @typeInfo(T).@"struct".field_names;
567         if (fields.len != semantic.len + derived.len + process.len) {
568             @compileError("classify every owner field before qualifying bytecode capture");
569         }
570         std.debug.assert(fields.len <= 64);
571         const Field = std.meta.FieldEnum(T);
572         var seen: u64 = 0;
573         for (.{ semantic, derived, process }) |group| {
574             for (group) |name| {
575                 const ordinal: u6 = @intCast(@backingInt(@field(Field, name)));
576                 const bit = @as(u64, 1) << ordinal;
577                 if (seen & bit != 0) @compileError("multiply classified owner field: " ++ name);
578                 seen |= bit;
579             }
580         }
581     }
582 }
583 
584 test "bytecode qualification classifies semantic derived and process storage fields" {
585     classify(ir.Operation, &.{
586         "name",       "location", "operands",   "results", "raw_dictionary_attrs",
587         "properties", "regions",  "successors",
588     }, &.{
589         "operand_values", "result_types", "parent_block", "prev_op", "next_op", "order",
590     }, &.{
591         "allocator",     "storage",       "operand_storage", "context", "lifecycle_state",
592         "tracking_prev", "tracking_next",
593     });
594     classify(ir.Block, &.{
595         "arguments", "argument_locations", "operations",
596     }, &.{ "parent", "prev", "next", "predecessors", "op_order_valid" }, &.{ "allocator", "id" });
597     classify(ir.Region, &.{"blocks"}, &.{"parent"}, &.{"allocator"});
598     classify(ir.Value, &.{"type"}, &.{ "kind", "first_use" }, &.{"id"});
599     classify(ir.Type, &.{"type_id"}, &.{}, &.{"impl"});
600     classify(ir.Type.DialectTypeStorage, &.{ "name", "param_key" }, &.{}, &.{
601         "type_info", "print_fn", "unique_id",
602     });
603     classify(ir.Attribute, &.{}, &.{}, &.{ "attr_id", "impl", "abstract" });
604     classify(ir.OpOperand, &.{"value"}, &.{
605         "owner", "operand_number", "operand_value_slot", "next_use", "back",
606     }, &.{});
607     classify(ir.Location.FileLocation, &.{ "filename", "line", "column" }, &.{}, &.{});
608     classify(ir.Location.FilePosition, &.{ "byte", "line", "column" }, &.{}, &.{});
609     classify(ir.Location.FileRangeLocation, &.{ "filename", "start", "end" }, &.{}, &.{});
610     classify(ir.Location.NameLocation, &.{ "name", "child" }, &.{}, &.{});
611     classify(ir.Location.FusedLocation, &.{ "locations", "metadata" }, &.{}, &.{});
612     classify(ir.Location.CallSiteLocation, &.{ "callee", "caller" }, &.{}, &.{});
613 }
614 
615 fn registerTestProperty(ctx: *ir.Context) !void {
616     _ = try ctx.registerOperation("test.property", .{});
617     try ctx.registerOperationInherentAttributeNames("test.property", &.{"value"});
618     try ctx.registerOperationPropertiesModel("test.property", ir.singleAttributePropertiesModel(
619         "test.property.storage",
620         "value",
621     ));
622 }
623 
624 test "bytecode qualification preserves complete single attribute properties and raw shadows" {
625     const allocator = std.testing.allocator;
626     var source_ctx = try testContext(allocator);
627     defer source_ctx.deinit(allocator);
628     var decode_ctx = try testContext(allocator);
629     defer decode_ctx.deinit(allocator);
630     try registerTestProperty(&source_ctx);
631     try registerTestProperty(&decode_ctx);
632     var state = ir.Operation.State.init("test.property", .unknown);
633     const original = try source_ctx.getI64Attr(11);
634     try state.setPropertiesAttr(original);
635     const shadow = try source_ctx.getI64Attr(99);
636     state.addRawAttributes(&.{.{ .name = "value", .value = shadow }});
637     const source = try source_ctx.createOperation(state);
638     const bytes = try encode(allocator, source, &.{}, &decode_ctx, test_limits);
639     defer allocator.free(bytes);
640     var decoded = try bytecode.decodeModule(allocator, &decode_ctx, bytes);
641     defer decoded.deinit();
642     try compare(allocator, source, decoded.module, &.{}, &.{}, test_limits);
643     const changed = try source_ctx.getI64Attr(12);
644     try source.setPropertiesFromAttr(changed);
645     try std.testing.expectError(error.UnencodableProduct, compare(
646         allocator,
647         source,
648         decoded.module,
649         &.{},
650         &.{},
651         test_limits,
652     ));
653     try std.testing.expectEqual(11, (try decoded.module.getPropertiesAsAttr()).?.cast(
654         ir.Attribute.IntegerAttr,
655     ).?.value);
656     try std.testing.expectEqual(99, decoded.module.raw_dictionary_attrs.get("value").?.cast(
657         ir.Attribute.IntegerAttr,
658     ).?.value);
659 }
660 
661 test "bytecode qualification canonicalizes resource maps and rejects duplicate identities" {
662     const allocator = std.testing.allocator;
663     var first_context = try testContext(allocator);
664     defer first_context.deinit(allocator);
665     var second_context = try testContext(allocator);
666     defer second_context.deinit(allocator);
667     var decode_context = try testContext(allocator);
668     defer decode_context.deinit(allocator);
669     const first = try testModule(&first_context);
670     const second = try testModule(&second_context);
671     const resources = [_]bytecode.Resource{
672         .{ .namespace = "z", .name = "a", .type_id = "bytes", .data = "second" },
673         .{ .namespace = "a", .name = "z", .type_id = "bytes", .data = "first" },
674     };
675     const reversed = [_]bytecode.Resource{ resources[1], resources[0] };
676     const a = try encode(allocator, first, &resources, &decode_context, test_limits);
677     defer allocator.free(a);
678     const b = try encode(allocator, second, &reversed, &decode_context, test_limits);
679     defer allocator.free(b);
680     try std.testing.expectEqualSlices(u8, a, b);
681     try std.testing.expectError(error.UnencodableProduct, encode(
682         allocator,
683         first,
684         &.{ resources[0], resources[0] },
685         &decode_context,
686         test_limits,
687     ));
688 }
689 
690 test "bytecode qualification rejects inconsistent derived argument and operand identities" {
691     const allocator = std.testing.allocator;
692     var context = try testContext(allocator);
693     defer context.deinit(allocator);
694     var decode_context = try testContext(allocator);
695     defer decode_context.deinit(allocator);
696     const source = try testModule(&context);
697     const block = source.getRegion(0).?.getEntryBlock().?;
698     const argument = block.arguments.items[0];
699     argument.kind.block_argument.arg_number = 1;
700     try std.testing.expectError(error.UnencodableProduct, encode(
701         allocator,
702         source,
703         &.{},
704         &decode_context,
705         test_limits,
706     ));
707     argument.kind.block_argument.arg_number = 0;
708     var operations = block.getOperations();
709     const operation = operations.next().?;
710     operation.operands.items[0].operand_number = 1;
711     try std.testing.expectError(error.UnencodableProduct, encode(
712         allocator,
713         source,
714         &.{},
715         &decode_context,
716         test_limits,
717     ));
718     operation.operands.items[0].operand_number = 0;
719     var limited = test_limits;
720     limited.depth = 0;
721     try std.testing.expectError(error.UnencodableProduct, encode(
722         allocator,
723         source,
724         &.{},
725         &decode_context,
726         limited,
727     ));
728 }