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 }