lib/choir/src/bytecode/bytecode.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const ir = @import("../core/root.zig");
   3 
   4 const magic = "SLIRBC1\x00";
   5 pub const format_version: u32 = 12;
   6 pub const minimum_readable_version: u32 = format_version;
   7 pub const maximum_readable_version: u32 = format_version;
   8 pub const default_dialect_version: u32 = 0;
   9 pub const default_dialect_flags: u32 = 0;
  10 const current_endianness: u8 = 1;
  11 
  12 pub const FormatVersionStatus = enum {
  13     readable,
  14     unsupported_older,
  15     unsupported_newer,
  16 };
  17 
  18 pub const FormatCompatibility = struct {
  19     version: u32,
  20     minimum_readable: u32 = minimum_readable_version,
  21     maximum_readable: u32 = maximum_readable_version,
  22     status: FormatVersionStatus,
  23 
  24     pub fn readable(self: FormatCompatibility) bool {
  25         return self.status == .readable;
  26     }
  27 };
  28 
  29 pub const ContainerHeader = struct {
  30     version: u32,
  31     endianness: u8,
  32     ptr_size: u8,
  33     flags: u16,
  34     section_table_offset: u64,
  35     section_count: u64,
  36     file_hash: u64,
  37 
  38     pub fn compatibility(self: ContainerHeader) FormatCompatibility {
  39         return classifyFormatVersion(self.version);
  40     }
  41 };
  42 
  43 pub fn classifyFormatVersion(version: u32) FormatCompatibility {
  44     return .{
  45         .version = version,
  46         .status = if (version < minimum_readable_version)
  47             .unsupported_older
  48         else if (version > maximum_readable_version)
  49             .unsupported_newer
  50         else
  51             .readable,
  52     };
  53 }
  54 
  55 pub const SectionKind = enum(u32) {
  56     strings = 1,
  57     dialects = 2,
  58     types = 3,
  59     attributes = 4,
  60     locations = 5,
  61     module = 6,
  62     resources = 7,
  63 };
  64 
  65 const SectionMeta = struct {
  66     kind: u32,
  67     flags: u32,
  68     offset: u64,
  69     size: u64,
  70 };
  71 
  72 const SectionData = struct {
  73     kind: u32,
  74     flags: u32,
  75     data: []const u8,
  76 };
  77 
  78 pub const DialectEntry = struct {
  79     name: []const u8,
  80     version: u32,
  81     flags: u32,
  82 
  83     pub fn usesDefaultVersion(self: DialectEntry) bool {
  84         return self.version == default_dialect_version and self.flags == default_dialect_flags;
  85     }
  86 };
  87 
  88 pub const Resource = struct {
  89     namespace: []const u8,
  90     name: []const u8,
  91     type_id: []const u8,
  92     data: []const u8,
  93 };
  94 
  95 pub const DecodedTables = struct {
  96     allocator: std.mem.Allocator,
  97     format: FormatCompatibility,
  98     strings: []const []const u8,
  99     dialects: []const DialectEntry,
 100     types: []const ir.Type,
 101     attrs: []const ir.Attribute,
 102     locs: []const ir.Location,
 103     loc_slices: std.ArrayList([]const ir.Location),
 104 
 105     pub fn deinit(self: *DecodedTables) void {
 106         for (self.loc_slices.items) |slice| {
 107             self.allocator.free(@constCast(slice));
 108         }
 109         self.loc_slices.deinit(self.allocator);
 110 
 111         for (self.strings) |str| {
 112             self.allocator.free(@constCast(str));
 113         }
 114         self.allocator.free(self.strings);
 115         self.allocator.free(self.dialects);
 116         self.allocator.free(self.types);
 117         self.allocator.free(self.attrs);
 118         self.allocator.free(self.locs);
 119     }
 120 };
 121 
 122 pub const DecodedModule = struct {
 123     module: *ir.Operation,
 124     tables: DecodedTables,
 125     resources: []const Resource = &.{},
 126 
 127     pub fn format(self: *const DecodedModule) FormatCompatibility {
 128         return self.tables.format;
 129     }
 130 
 131     pub fn deinit(self: *DecodedModule) void {
 132         freeResources(self.tables.allocator, self.resources);
 133         self.tables.deinit();
 134     }
 135 };
 136 
 137 pub const FlatModuleDecoder = struct {
 138     allocator: std.mem.Allocator,
 139     tables: DecodedTables,
 140     resources: []const Resource,
 141     decoder: ModuleDecoder,
 142     reader: SliceReader,
 143     module_name: []const u8,
 144     module_location: ir.Location,
 145     operations_remaining: usize,
 146     value_base: usize = 0,
 147     current: ?*ir.Operation = null,
 148 
 149     pub fn init(
 150         allocator: std.mem.Allocator,
 151         ctx: *ir.Context,
 152         bytes: []const u8,
 153     ) !FlatModuleDecoder {
 154         const sections = try parseSections(bytes);
 155         const module_bytes = sections.module orelse return error.MissingModule;
 156 
 157         var tables = try decodeTablesFromSections(allocator, ctx, sections);
 158         errdefer tables.deinit();
 159 
 160         const resources = try decodeResourcesSection(allocator, sections.resources);
 161         errdefer freeResources(allocator, resources);
 162 
 163         var decoder = ModuleDecoder.init(allocator, ctx, &tables);
 164         errdefer decoder.deinit();
 165 
 166         var reader = SliceReader.init(module_bytes);
 167         const envelope = try decodeFlatModuleEnvelope(&decoder, &reader);
 168         return .{
 169             .allocator = allocator,
 170             .tables = tables,
 171             .resources = resources,
 172             .decoder = decoder,
 173             .reader = reader,
 174             .module_name = envelope.name,
 175             .module_location = envelope.location,
 176             .operations_remaining = envelope.operation_count,
 177         };
 178     }
 179 
 180     pub fn deinit(self: *FlatModuleDecoder) void {
 181         self.releaseCurrent();
 182         self.decoder.deinit();
 183         freeResources(self.allocator, self.resources);
 184         self.tables.deinit();
 185         self.* = undefined;
 186     }
 187 
 188     pub fn format(self: *const FlatModuleDecoder) FormatCompatibility {
 189         return self.tables.format;
 190     }
 191 
 192     pub fn name(self: *const FlatModuleDecoder) []const u8 {
 193         return self.module_name;
 194     }
 195 
 196     pub fn location(self: *const FlatModuleDecoder) ir.Location {
 197         return self.module_location;
 198     }
 199 
 200     pub fn next(self: *FlatModuleDecoder) !?*ir.Operation {
 201         self.releaseCurrent();
 202         if (self.operations_remaining == 0) {
 203             if (self.reader.offset != self.reader.bytes.len) {
 204                 return error.InvalidModule;
 205             }
 206             return null;
 207         }
 208         self.decoder.value_base = self.value_base;
 209         const op = try decodeOperation(
 210             &self.decoder,
 211             &self.reader,
 212             null,
 213             null,
 214         );
 215         self.current = op;
 216         self.operations_remaining -= 1;
 217         return op;
 218     }
 219 
 220     fn releaseCurrent(self: *FlatModuleDecoder) void {
 221         const op = self.current orelse return;
 222         op.erase();
 223         self.value_base = std.math.add(
 224             usize,
 225             self.value_base,
 226             self.decoder.values.items.len,
 227         ) catch unreachable;
 228         self.decoder.values.clearRetainingCapacity();
 229         self.current = null;
 230     }
 231 };
 232 
 233 const StringTable = struct {
 234     allocator: std.mem.Allocator,
 235     items: std.ArrayList([]const u8),
 236     map: std.StringHashMap(u32),
 237 
 238     pub fn init(allocator: std.mem.Allocator) StringTable {
 239         return .{
 240             .allocator = allocator,
 241             .items = .empty,
 242             .map = std.StringHashMap(u32).init(allocator),
 243         };
 244     }
 245 
 246     pub fn deinit(self: *StringTable) void {
 247         for (self.items.items) |value| {
 248             self.allocator.free(@constCast(value));
 249         }
 250         self.items.deinit(self.allocator);
 251         self.map.deinit();
 252     }
 253 
 254     pub fn intern(self: *StringTable, value: []const u8) !u32 {
 255         if (self.map.get(value)) |id| return id;
 256         const id = std.math.cast(u32, self.items.items.len) orelse return error.TableLimit;
 257         try self.items.ensureUnusedCapacity(self.allocator, 1);
 258         try self.map.ensureUnusedCapacity(1);
 259         const owned = try self.allocator.dupe(u8, value);
 260         self.items.appendAssumeCapacity(owned);
 261         self.map.putAssumeCapacity(owned, id);
 262         return id;
 263     }
 264 };
 265 
 266 fn stringAllocationScenario(allocator: std.mem.Allocator) !void {
 267     var table = StringTable.init(allocator);
 268     defer table.deinit();
 269     errdefer std.debug.assert(table.items.items.len == table.map.count());
 270     for ([_][]const u8{ "a", "b", "c", "d", "e", "f", "g", "h", "i" }, 0..) |value, ordinal| {
 271         try std.testing.expectEqual(ordinal, try table.intern(value));
 272     }
 273     try std.testing.expectEqual(0, try table.intern("a"));
 274 }
 275 
 276 test "bytecode string interning owns a consistent prefix across allocation failures" {
 277     try std.testing.checkAllAllocationFailures(std.testing.allocator, stringAllocationScenario, .{});
 278 }
 279 
 280 const DialectRecord = struct {
 281     name_id: u32,
 282     version: u32,
 283     flags: u32,
 284 };
 285 
 286 const DialectTable = struct {
 287     items: std.ArrayList(DialectRecord),
 288     map: std.AutoHashMap(u32, u32),
 289 
 290     pub fn init(allocator: std.mem.Allocator) DialectTable {
 291         return .{
 292             .items = .empty,
 293             .map = std.AutoHashMap(u32, u32).init(allocator),
 294         };
 295     }
 296 
 297     pub fn deinit(self: *DialectTable, allocator: std.mem.Allocator) void {
 298         self.items.deinit(allocator);
 299         self.map.deinit();
 300     }
 301 
 302     pub fn intern(self: *DialectTable, allocator: std.mem.Allocator, name_id: u32) !u32 {
 303         if (self.map.get(name_id)) |id| return id;
 304         try self.items.append(allocator, .{
 305             .name_id = name_id,
 306             .version = default_dialect_version,
 307             .flags = default_dialect_flags,
 308         });
 309         const id: u32 = @intCast(self.items.items.len - 1);
 310         try self.map.put(name_id, id);
 311         return id;
 312     }
 313 };
 314 
 315 pub const TableBuilder = struct {
 316     allocator: std.mem.Allocator,
 317     strings: StringTable,
 318     dialects: DialectTable,
 319     types: std.ArrayList(ir.Type),
 320     attrs: std.ArrayList(ir.Attribute),
 321     locs: std.ArrayList(ir.Location),
 322 
 323     pub fn init(allocator: std.mem.Allocator) TableBuilder {
 324         return .{
 325             .allocator = allocator,
 326             .strings = StringTable.init(allocator),
 327             .dialects = DialectTable.init(allocator),
 328             .types = .empty,
 329             .attrs = .empty,
 330             .locs = .empty,
 331         };
 332     }
 333 
 334     pub fn deinit(self: *TableBuilder) void {
 335         self.locs.deinit(self.allocator);
 336         self.attrs.deinit(self.allocator);
 337         self.types.deinit(self.allocator);
 338         self.dialects.deinit(self.allocator);
 339         self.strings.deinit();
 340     }
 341 
 342     pub fn internString(self: *TableBuilder, value: []const u8) !u32 {
 343         return self.strings.intern(value);
 344     }
 345 
 346     pub fn internDialect(self: *TableBuilder, name: []const u8) !u32 {
 347         const name_id = try self.internString(name);
 348         return self.dialects.intern(self.allocator, name_id);
 349     }
 350 
 351     pub fn internType(self: *TableBuilder, typ: ir.Type) !u32 {
 352         if (findTypeIndex(self.types.items, typ)) |id| return id;
 353         const storage = typ.getDialectStorage() orelse return error.UnsupportedType;
 354         const dialect = dialectNamespace(storage.name);
 355         if (dialect.len > 0) {
 356             _ = try self.internDialect(dialect);
 357             const type_name = dialectTypeName(storage.name);
 358             _ = try self.internString(type_name);
 359         } else {
 360             _ = try self.internDialect(storage.name);
 361         }
 362         if (storage.param_key.len > 0) {
 363             _ = try self.internString(storage.param_key);
 364         }
 365 
 366         try self.types.append(self.allocator, typ);
 367         return @intCast(self.types.items.len - 1);
 368     }
 369 
 370     pub fn internAttribute(self: *TableBuilder, attr: ir.Attribute) !u32 {
 371         if (findAttrIndex(self.attrs.items, attr)) |id| return id;
 372         if (attr.abstract.name.len == 0) return error.UnsupportedAttribute;
 373 
 374         const dialect = dialectNamespace(attr.abstract.name);
 375         if (dialect.len > 0) {
 376             _ = try self.internDialect(dialect);
 377         }
 378         const attr_name = dialectTypeName(attr.abstract.name);
 379         _ = try self.internString(attr_name);
 380 
 381         const is_typed = std.mem.eql(u8, attr.abstract.name, ir.builtin_attr_names.integer) or
 382             std.mem.eql(u8, attr.abstract.name, ir.builtin_attr_names.float_) or
 383             std.mem.eql(u8, attr.abstract.name, ir.builtin_attr_names.bool_) or
 384             std.mem.eql(u8, attr.abstract.name, ir.builtin_attr_names.string) or
 385             std.mem.eql(u8, attr.abstract.name, ir.builtin_attr_names.symbol_ref) or
 386             std.mem.eql(u8, attr.abstract.name, ir.builtin_attr_names.string_list) or
 387             std.mem.eql(u8, attr.abstract.name, ir.builtin_attr_names.type_list) or
 388             std.mem.eql(u8, attr.abstract.name, ir.builtin_attr_names.array);
 389         if (!is_typed) {
 390             _ = attr.cast(ir.Attribute.DialectAttr) orelse return error.UnsupportedAttribute;
 391         }
 392         if (std.mem.eql(u8, attr.abstract.name, ir.builtin_attr_names.type_list)) {
 393             const type_list_attr = attr.cast(ir.Attribute.TypeListAttr) orelse return error.UnsupportedAttribute;
 394             for (type_list_attr.values) |typ| {
 395                 _ = try self.internType(typ);
 396             }
 397         }
 398         if (std.mem.eql(u8, attr.abstract.name, ir.builtin_attr_names.array)) {
 399             const array_attr = attr.cast(ir.Attribute.ArrayAttr) orelse return error.UnsupportedAttribute;
 400             for (array_attr.values) |child| {
 401                 _ = try self.internAttribute(child);
 402             }
 403         }
 404 
 405         try self.attrs.append(self.allocator, attr);
 406         return @intCast(self.attrs.items.len - 1);
 407     }
 408 
 409     pub fn internLocation(self: *TableBuilder, loc: ir.Location) !u32 {
 410         if (findLocationIndex(self.locs.items, loc)) |id| return id;
 411 
 412         switch (loc) {
 413             .unknown => {},
 414             .file => |file_loc| {
 415                 _ = try self.internString(file_loc.filename);
 416             },
 417             .file_range => |file_range| {
 418                 _ = try self.internString(file_range.filename);
 419             },
 420             .name => |name_loc| {
 421                 _ = try self.internString(name_loc.name);
 422                 if (name_loc.child) |child| {
 423                     _ = try self.internLocation(child.*);
 424                 }
 425             },
 426             .fused => |fused| {
 427                 for (fused.locations) |child_loc| {
 428                     _ = try self.internLocation(child_loc);
 429                 }
 430             },
 431             .call_site => |call_site| {
 432                 _ = try self.internLocation(call_site.callee.*);
 433                 _ = try self.internLocation(call_site.caller.*);
 434             },
 435         }
 436 
 437         try self.locs.append(self.allocator, loc);
 438         return @intCast(self.locs.items.len - 1);
 439     }
 440 };
 441 
 442 pub fn encodeTables(allocator: std.mem.Allocator, builder: *TableBuilder) ![]u8 {
 443     var sections: std.ArrayList(SectionData) = .empty;
 444     defer {
 445         for (sections.items) |section| {
 446             allocator.free(@constCast(section.data));
 447         }
 448         sections.deinit(allocator);
 449     }
 450 
 451     const strings_data = try encodeStringsSection(allocator, builder);
 452     try sections.append(allocator, .{ .kind = @backingInt(SectionKind.strings), .flags = 0, .data = strings_data });
 453 
 454     const dialects_data = try encodeDialectsSection(allocator, builder);
 455     try sections.append(allocator, .{ .kind = @backingInt(SectionKind.dialects), .flags = 0, .data = dialects_data });
 456 
 457     const types_data = try encodeTypesSection(allocator, builder);
 458     try sections.append(allocator, .{ .kind = @backingInt(SectionKind.types), .flags = 0, .data = types_data });
 459 
 460     const attrs_data = try encodeAttributesSection(allocator, builder);
 461     try sections.append(allocator, .{ .kind = @backingInt(SectionKind.attributes), .flags = 0, .data = attrs_data });
 462 
 463     const locs_data = try encodeLocationsSection(allocator, builder);
 464     try sections.append(allocator, .{ .kind = @backingInt(SectionKind.locations), .flags = 0, .data = locs_data });
 465 
 466     return try writeContainer(allocator, sections.items);
 467 }
 468 
 469 pub fn encodeModule(allocator: std.mem.Allocator, module: *ir.Operation) ![]u8 {
 470     return encodeModuleWithResources(allocator, module, &.{});
 471 }
 472 
 473 pub fn encodeModuleWithResources(
 474     allocator: std.mem.Allocator,
 475     module: *ir.Operation,
 476     resources: []const Resource,
 477 ) ![]u8 {
 478     var builder = TableBuilder.init(allocator);
 479     defer builder.deinit();
 480 
 481     _ = try builder.internLocation(ir.Location.getUnknown());
 482     try collectOperationTables(&builder, module);
 483 
 484     var sections: std.ArrayList(SectionData) = .empty;
 485     defer {
 486         for (sections.items) |section| {
 487             allocator.free(@constCast(section.data));
 488         }
 489         sections.deinit(allocator);
 490     }
 491 
 492     try appendOwnedSection(
 493         allocator,
 494         &sections,
 495         @backingInt(SectionKind.strings),
 496         try encodeStringsSection(allocator, &builder),
 497     );
 498 
 499     try appendOwnedSection(
 500         allocator,
 501         &sections,
 502         @backingInt(SectionKind.dialects),
 503         try encodeDialectsSection(allocator, &builder),
 504     );
 505 
 506     try appendOwnedSection(
 507         allocator,
 508         &sections,
 509         @backingInt(SectionKind.types),
 510         try encodeTypesSection(allocator, &builder),
 511     );
 512 
 513     try appendOwnedSection(
 514         allocator,
 515         &sections,
 516         @backingInt(SectionKind.attributes),
 517         try encodeAttributesSection(allocator, &builder),
 518     );
 519 
 520     try appendOwnedSection(
 521         allocator,
 522         &sections,
 523         @backingInt(SectionKind.locations),
 524         try encodeLocationsSection(allocator, &builder),
 525     );
 526 
 527     try appendOwnedSection(
 528         allocator,
 529         &sections,
 530         @backingInt(SectionKind.module),
 531         try encodeModuleSection(allocator, &builder, module),
 532     );
 533 
 534     if (resources.len != 0) {
 535         try appendOwnedSection(
 536             allocator,
 537             &sections,
 538             @backingInt(SectionKind.resources),
 539             try encodeResourcesSection(allocator, resources),
 540         );
 541     }
 542 
 543     return try writeContainer(allocator, sections.items);
 544 }
 545 
 546 fn appendOwnedSection(
 547     allocator: std.mem.Allocator,
 548     sections: *std.ArrayList(SectionData),
 549     kind: u32,
 550     data: []const u8,
 551 ) !void {
 552     errdefer allocator.free(@constCast(data));
 553     try sections.append(allocator, .{ .kind = kind, .flags = 0, .data = data });
 554 }
 555 
 556 pub fn decodeTables(allocator: std.mem.Allocator, ctx: *ir.Context, bytes: []const u8) !DecodedTables {
 557     const sections = try parseSections(bytes);
 558     return decodeTablesFromSections(allocator, ctx, sections);
 559 }
 560 
 561 fn decodeTablesFromSections(
 562     allocator: std.mem.Allocator,
 563     ctx: *ir.Context,
 564     sections: ParsedSections,
 565 ) !DecodedTables {
 566     const strings_bytes = sections.strings orelse return error.MissingStrings;
 567     const strings = try decodeStrings(allocator, strings_bytes);
 568     errdefer freeStringTable(allocator, strings);
 569 
 570     const dialects = try decodeDialects(allocator, sections.dialects, strings);
 571     errdefer allocator.free(dialects);
 572 
 573     const types = try decodeTypes(allocator, ctx, sections.types, strings, dialects);
 574     errdefer allocator.free(types);
 575 
 576     const attrs = try decodeAttributes(allocator, ctx, sections.attrs, strings, dialects, types);
 577     errdefer allocator.free(attrs);
 578 
 579     var loc_slices: std.ArrayList([]const ir.Location) = .empty;
 580     errdefer {
 581         for (loc_slices.items) |slice| {
 582             allocator.free(@constCast(slice));
 583         }
 584         loc_slices.deinit(allocator);
 585     }
 586 
 587     const locs = try decodeLocations(allocator, sections.locs, strings, &loc_slices);
 588     errdefer allocator.free(locs);
 589 
 590     return .{
 591         .allocator = allocator,
 592         .format = sections.header.compatibility(),
 593         .strings = strings,
 594         .dialects = dialects,
 595         .types = types,
 596         .attrs = attrs,
 597         .locs = locs,
 598         .loc_slices = loc_slices,
 599     };
 600 }
 601 
 602 pub fn decodeModule(allocator: std.mem.Allocator, ctx: *ir.Context, bytes: []const u8) !DecodedModule {
 603     const sections = try parseSections(bytes);
 604     const module_bytes = sections.module orelse return error.MissingModule;
 605 
 606     var tables = try decodeTablesFromSections(allocator, ctx, sections);
 607     errdefer tables.deinit();
 608 
 609     const resources = try decodeResourcesSection(allocator, sections.resources);
 610     errdefer freeResources(allocator, resources);
 611 
 612     const module_op = try decodeModuleSection(allocator, ctx, module_bytes, &tables);
 613     return .{
 614         .module = module_op,
 615         .tables = tables,
 616         .resources = resources,
 617     };
 618 }
 619 
 620 fn headerSize() u64 {
 621     return 8 + 4 + 1 + 1 + 2 + 8 + 8 + 8;
 622 }
 623 
 624 fn sectionEntrySize() u64 {
 625     return 4 + 4 + 8 + 8;
 626 }
 627 
 628 fn writeContainer(allocator: std.mem.Allocator, sections: []const SectionData) ![]u8 {
 629     const section_count: u64 = @intCast(sections.len);
 630     const table_offset = headerSize();
 631     const table_size = section_count * sectionEntrySize();
 632 
 633     var metas = try allocator.alloc(SectionMeta, sections.len);
 634     defer allocator.free(metas);
 635 
 636     var cursor = table_offset + table_size;
 637     for (sections, 0..) |section, idx| {
 638         metas[idx] = .{
 639             .kind = section.kind,
 640             .flags = section.flags,
 641             .offset = cursor,
 642             .size = @intCast(section.data.len),
 643         };
 644         cursor += @intCast(section.data.len);
 645     }
 646 
 647     var writer = BufferWriter.init(allocator);
 648     errdefer writer.deinit();
 649 
 650     try writer.writeAll(magic);
 651     try writer.writeU32(format_version);
 652     try writer.writeByte(current_endianness);
 653     try writer.writeByte(@intCast(@sizeOf(usize)));
 654     try writer.writeU16(0);
 655     try writer.writeU64(table_offset);
 656     try writer.writeU64(section_count);
 657     try writer.writeU64(0);
 658 
 659     for (metas) |meta| {
 660         try writer.writeU32(meta.kind);
 661         try writer.writeU32(meta.flags);
 662         try writer.writeU64(meta.offset);
 663         try writer.writeU64(meta.size);
 664     }
 665 
 666     for (sections) |section| {
 667         try writer.writeAll(section.data);
 668     }
 669 
 670     return writer.toOwnedSlice();
 671 }
 672 
 673 pub fn inspectHeader(bytes: []const u8) !ContainerHeader {
 674     var reader = SliceReader.init(bytes);
 675     return readHeader(&reader);
 676 }
 677 
 678 fn readHeader(reader: *SliceReader) !ContainerHeader {
 679     const got_magic = try reader.readBytes(magic.len);
 680     if (!std.mem.eql(u8, got_magic, magic)) return error.InvalidHeader;
 681 
 682     return .{
 683         .version = try reader.readU32(),
 684         .endianness = try reader.readByte(),
 685         .ptr_size = try reader.readByte(),
 686         .flags = try reader.readU16(),
 687         .section_table_offset = try reader.readU64(),
 688         .section_count = try reader.readU64(),
 689         .file_hash = try reader.readU64(),
 690     };
 691 }
 692 
 693 pub const ParsedSections = struct {
 694     header: ContainerHeader,
 695     strings: ?[]const u8,
 696     dialects: ?[]const u8,
 697     types: ?[]const u8,
 698     attrs: ?[]const u8,
 699     locs: ?[]const u8,
 700     module: ?[]const u8,
 701     resources: ?[]const u8,
 702 };
 703 
 704 pub fn parseSections(bytes: []const u8) !ParsedSections {
 705     var header_reader = SliceReader.init(bytes);
 706     const header = try readHeader(&header_reader);
 707 
 708     if (!header.compatibility().readable()) return error.UnsupportedVersion;
 709     if (header.endianness != current_endianness) return error.UnsupportedEndianness;
 710 
 711     const bytes_len_u64: u64 = @intCast(bytes.len);
 712     const section_table_size = std.math.mul(
 713         u64,
 714         header.section_count,
 715         sectionEntrySize(),
 716     ) catch return error.InvalidSectionTable;
 717     const section_table_end = std.math.add(
 718         u64,
 719         header.section_table_offset,
 720         section_table_size,
 721     ) catch return error.InvalidSectionTable;
 722     if (section_table_end > bytes_len_u64) return error.InvalidSectionTable;
 723 
 724     var sections = ParsedSections{
 725         .header = header,
 726         .strings = null,
 727         .dialects = null,
 728         .types = null,
 729         .attrs = null,
 730         .locs = null,
 731         .module = null,
 732         .resources = null,
 733     };
 734 
 735     const table_offset: usize = @intCast(header.section_table_offset);
 736     var table_reader = SliceReader.init(bytes[table_offset..]);
 737     var i: u64 = 0;
 738     while (i < header.section_count) : (i += 1) {
 739         const kind_raw = try table_reader.readU32();
 740         const flags = try table_reader.readU32();
 741         const offset = try table_reader.readU64();
 742         const size = try table_reader.readU64();
 743         _ = flags;
 744 
 745         const end_u64 = std.math.add(
 746             u64,
 747             offset,
 748             size,
 749         ) catch return error.InvalidSection;
 750         if (end_u64 > bytes_len_u64) return error.InvalidSection;
 751         const start: usize = @intCast(offset);
 752         const end: usize = @intCast(end_u64);
 753         const slice = bytes[start..end];
 754 
 755         switch (kind_raw) {
 756             @backingInt(SectionKind.strings) => {
 757                 if (sections.strings != null) return error.DuplicateSection;
 758                 sections.strings = slice;
 759             },
 760             @backingInt(SectionKind.dialects) => {
 761                 if (sections.dialects != null) return error.DuplicateSection;
 762                 sections.dialects = slice;
 763             },
 764             @backingInt(SectionKind.types) => {
 765                 if (sections.types != null) return error.DuplicateSection;
 766                 sections.types = slice;
 767             },
 768             @backingInt(SectionKind.attributes) => {
 769                 if (sections.attrs != null) return error.DuplicateSection;
 770                 sections.attrs = slice;
 771             },
 772             @backingInt(SectionKind.locations) => {
 773                 if (sections.locs != null) return error.DuplicateSection;
 774                 sections.locs = slice;
 775             },
 776             @backingInt(SectionKind.module) => {
 777                 if (sections.module != null) return error.DuplicateSection;
 778                 sections.module = slice;
 779             },
 780             @backingInt(SectionKind.resources) => {
 781                 if (sections.resources != null) return error.DuplicateSection;
 782                 sections.resources = slice;
 783             },
 784             else => {},
 785         }
 786     }
 787 
 788     return sections;
 789 }
 790 
 791 fn encodeStringsSection(allocator: std.mem.Allocator, builder: *TableBuilder) ![]u8 {
 792     var writer = BufferWriter.init(allocator);
 793     errdefer writer.deinit();
 794 
 795     try writer.writeULEB(builder.strings.items.items.len);
 796     for (builder.strings.items.items) |value| {
 797         try writer.writeBytes(value);
 798     }
 799 
 800     return writer.toOwnedSlice();
 801 }
 802 
 803 fn encodeDialectsSection(allocator: std.mem.Allocator, builder: *TableBuilder) ![]u8 {
 804     var writer = BufferWriter.init(allocator);
 805     errdefer writer.deinit();
 806 
 807     try writer.writeULEB(builder.dialects.items.items.len);
 808     for (builder.dialects.items.items) |dialect| {
 809         try writer.writeULEB(dialect.name_id);
 810         try writer.writeULEB(dialect.version);
 811         try writer.writeULEB(dialect.flags);
 812     }
 813 
 814     return writer.toOwnedSlice();
 815 }
 816 
 817 fn encodeTypesSection(allocator: std.mem.Allocator, builder: *TableBuilder) ![]u8 {
 818     var writer = BufferWriter.init(allocator);
 819     errdefer writer.deinit();
 820 
 821     try writer.writeULEB(builder.types.items.len);
 822     for (builder.types.items) |typ| {
 823         try encodeType(builder, &writer, typ);
 824     }
 825 
 826     return writer.toOwnedSlice();
 827 }
 828 
 829 fn encodeAttributesSection(allocator: std.mem.Allocator, builder: *TableBuilder) ![]u8 {
 830     var writer = BufferWriter.init(allocator);
 831     errdefer writer.deinit();
 832 
 833     try writer.writeULEB(builder.attrs.items.len);
 834     for (builder.attrs.items) |attr| {
 835         try encodeAttribute(builder, &writer, attr);
 836     }
 837 
 838     return writer.toOwnedSlice();
 839 }
 840 
 841 fn encodeLocationsSection(allocator: std.mem.Allocator, builder: *TableBuilder) ![]u8 {
 842     var writer = BufferWriter.init(allocator);
 843     errdefer writer.deinit();
 844 
 845     try writer.writeULEB(builder.locs.items.len);
 846     for (builder.locs.items) |loc| {
 847         try encodeLocation(builder, &writer, loc);
 848     }
 849 
 850     return writer.toOwnedSlice();
 851 }
 852 
 853 fn encodeResourcesSection(
 854     allocator: std.mem.Allocator,
 855     resources: []const Resource,
 856 ) ![]u8 {
 857     var writer = BufferWriter.init(allocator);
 858     errdefer writer.deinit();
 859 
 860     try writer.writeULEB(resources.len);
 861     for (resources) |resource| {
 862         try writer.writeBytes(resource.namespace);
 863         try writer.writeBytes(resource.name);
 864         try writer.writeBytes(resource.type_id);
 865         try writer.writeBytes(resource.data);
 866     }
 867 
 868     return writer.toOwnedSlice();
 869 }
 870 
 871 fn decodeResourcesSection(
 872     allocator: std.mem.Allocator,
 873     bytes_opt: ?[]const u8,
 874 ) ![]const Resource {
 875     const bytes = bytes_opt orelse return &.{};
 876 
 877     var reader = SliceReader.init(bytes);
 878     const count = try reader.readULEBToUsize();
 879     if (count == 0) {
 880         if (reader.offset != bytes.len) return error.InvalidSection;
 881         return &.{};
 882     }
 883 
 884     const resources = try allocator.alloc(Resource, count);
 885     var initialized: usize = 0;
 886     errdefer {
 887         for (resources[0..initialized]) |resource| {
 888             allocator.free(@constCast(resource.namespace));
 889             allocator.free(@constCast(resource.name));
 890             allocator.free(@constCast(resource.type_id));
 891             allocator.free(@constCast(resource.data));
 892         }
 893         if (resources.len != 0) allocator.free(resources);
 894     }
 895 
 896     while (initialized < count) : (initialized += 1) {
 897         const namespace = try reader.readBytesWithLen();
 898         const name = try reader.readBytesWithLen();
 899         const type_id = try reader.readBytesWithLen();
 900         const data = try reader.readBytesWithLen();
 901         const namespace_owned = try allocator.dupe(u8, namespace);
 902         errdefer allocator.free(namespace_owned);
 903         const name_owned = try allocator.dupe(u8, name);
 904         errdefer allocator.free(name_owned);
 905         const type_id_owned = try allocator.dupe(u8, type_id);
 906         errdefer allocator.free(type_id_owned);
 907         const data_owned = try allocator.dupe(u8, data);
 908         errdefer allocator.free(data_owned);
 909         resources[initialized] = .{
 910             .namespace = namespace_owned,
 911             .name = name_owned,
 912             .type_id = type_id_owned,
 913             .data = data_owned,
 914         };
 915     }
 916     if (reader.offset != bytes.len) return error.InvalidSection;
 917 
 918     return resources;
 919 }
 920 
 921 fn freeResources(allocator: std.mem.Allocator, resources: []const Resource) void {
 922     for (resources) |resource| {
 923         allocator.free(@constCast(resource.namespace));
 924         allocator.free(@constCast(resource.name));
 925         allocator.free(@constCast(resource.type_id));
 926         allocator.free(@constCast(resource.data));
 927     }
 928     if (resources.len != 0) allocator.free(@constCast(resources));
 929 }
 930 
 931 fn decodeStrings(allocator: std.mem.Allocator, bytes: []const u8) ![]const []const u8 {
 932     var reader = SliceReader.init(bytes);
 933     const count = try reader.readULEBToUsize();
 934 
 935     const strings = try allocator.alloc([]const u8, count);
 936     var initialized: usize = 0;
 937     errdefer {
 938         for (strings[0..initialized]) |value| allocator.free(value);
 939         allocator.free(strings);
 940     }
 941     for (strings) |*slot| {
 942         const slice = try reader.readBytesWithLen();
 943         const owned = try allocator.dupe(u8, slice);
 944         slot.* = owned;
 945         initialized += 1;
 946     }
 947 
 948     return strings;
 949 }
 950 
 951 fn decodeDialects(
 952     allocator: std.mem.Allocator,
 953     bytes_opt: ?[]const u8,
 954     strings: []const []const u8,
 955 ) ![]const DialectEntry {
 956     if (bytes_opt == null) return allocator.alloc(DialectEntry, 0);
 957 
 958     var reader = SliceReader.init(bytes_opt.?);
 959     const count = try reader.readULEBToUsize();
 960     const dialects = try allocator.alloc(DialectEntry, count);
 961     errdefer allocator.free(dialects);
 962     for (dialects) |*slot| {
 963         const name_id = try reader.readULEBToUsize();
 964         const version = try reader.readULEBToU32();
 965         const flags = try reader.readULEBToU32();
 966         if (name_id >= strings.len) return error.InvalidTable;
 967         slot.* = .{
 968             .name = strings[name_id],
 969             .version = version,
 970             .flags = flags,
 971         };
 972     }
 973 
 974     return dialects;
 975 }
 976 
 977 fn decodeTypes(
 978     allocator: std.mem.Allocator,
 979     ctx: *ir.Context,
 980     bytes_opt: ?[]const u8,
 981     strings: []const []const u8,
 982     dialects: []const DialectEntry,
 983 ) ![]const ir.Type {
 984     if (bytes_opt == null) return allocator.alloc(ir.Type, 0);
 985 
 986     var reader = SliceReader.init(bytes_opt.?);
 987     const count = try reader.readULEBToUsize();
 988     const types = try allocator.alloc(ir.Type, count);
 989     errdefer allocator.free(types);
 990     for (types) |*slot| {
 991         slot.* = try decodeType(ctx, &reader, strings, dialects, types);
 992     }
 993 
 994     return types;
 995 }
 996 
 997 fn decodeAttributes(
 998     allocator: std.mem.Allocator,
 999     ctx: *ir.Context,
1000     bytes_opt: ?[]const u8,
1001     strings: []const []const u8,
1002     dialects: []const DialectEntry,
1003     types: []const ir.Type,
1004 ) ![]const ir.Attribute {
1005     if (bytes_opt == null) return allocator.alloc(ir.Attribute, 0);
1006 
1007     var reader = SliceReader.init(bytes_opt.?);
1008     const count = try reader.readULEBToUsize();
1009     const attrs = try allocator.alloc(ir.Attribute, count);
1010     errdefer allocator.free(attrs);
1011     for (attrs, 0..) |*slot, index| {
1012         slot.* = try decodeAttribute(allocator, ctx, &reader, strings, dialects, types, attrs[0..index]);
1013     }
1014 
1015     return attrs;
1016 }
1017 
1018 fn decodeLocations(
1019     allocator: std.mem.Allocator,
1020     bytes_opt: ?[]const u8,
1021     strings: []const []const u8,
1022     loc_slices: *std.ArrayList([]const ir.Location),
1023 ) ![]const ir.Location {
1024     if (bytes_opt == null) return allocator.alloc(ir.Location, 0);
1025 
1026     var reader = SliceReader.init(bytes_opt.?);
1027     const count = try reader.readULEBToUsize();
1028     const locs = try allocator.alloc(ir.Location, count);
1029     errdefer allocator.free(locs);
1030 
1031     var idx: usize = 0;
1032     while (idx < count) : (idx += 1) {
1033         locs[idx] = try decodeLocation(allocator, &reader, strings, locs, loc_slices);
1034     }
1035 
1036     return locs;
1037 }
1038 
1039 fn freeStringTable(allocator: std.mem.Allocator, strings: []const []const u8) void {
1040     for (strings) |value| {
1041         allocator.free(@constCast(value));
1042     }
1043     allocator.free(strings);
1044 }
1045 
1046 fn decodeType(
1047     ctx: *ir.Context,
1048     reader: *SliceReader,
1049     strings: []const []const u8,
1050     dialects: []const DialectEntry,
1051     decoded: []const ir.Type,
1052 ) !ir.Type {
1053     _ = decoded;
1054     const kind = try reader.readByte();
1055     switch (kind) {
1056         @backingInt(TypeKind.builtin_scalar) => {
1057             const scalar_kind = std.enums.fromInt(BuiltinScalarKind, try reader.readByte()) orelse
1058                 return error.UnsupportedType;
1059             const width = try reader.readByte();
1060             const name = builtinScalarName(scalar_kind, width) orelse return error.UnsupportedType;
1061             return ctx.getDialectTypeFromName(name);
1062         },
1063         @backingInt(TypeKind.dialect) => {
1064             const dialect_id = try reader.readULEBToUsize();
1065             const type_name_id = try reader.readULEBToUsize();
1066             const has_key = (try reader.readByte()) != 0;
1067             const key_id = if (has_key) try reader.readULEBToUsize() else 0;
1068             if (dialect_id >= dialects.len) return error.InvalidTable;
1069             if (type_name_id >= strings.len) return error.InvalidTable;
1070             if (has_key and key_id >= strings.len) return error.InvalidTable;
1071 
1072             const dialect = dialects[dialect_id].name;
1073             const type_name = strings[type_name_id];
1074             const param_key = if (has_key) strings[key_id] else "";
1075             const allocator = ir.context.transientAllocator(ctx);
1076             const full_name = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ dialect, type_name });
1077             defer allocator.free(full_name);
1078             return ctx.getDialectTypeFromNameWithKey(full_name, param_key);
1079         },
1080         @backingInt(TypeKind.dialect_only) => {
1081             const dialect_id = try reader.readULEBToUsize();
1082             const has_key = (try reader.readByte()) != 0;
1083             const key_id = if (has_key) try reader.readULEBToUsize() else 0;
1084             if (dialect_id >= dialects.len) return error.InvalidTable;
1085             if (has_key and key_id >= strings.len) return error.InvalidTable;
1086 
1087             const dialect = dialects[dialect_id].name;
1088             const param_key = if (has_key) strings[key_id] else "";
1089             return ctx.getDialectTypeFromNameWithKey(dialect, param_key);
1090         },
1091         else => return error.UnsupportedType,
1092     }
1093 }
1094 
1095 fn decodeAttribute(
1096     allocator: std.mem.Allocator,
1097     ctx: *ir.Context,
1098     reader: *SliceReader,
1099     strings: []const []const u8,
1100     dialects: []const DialectEntry,
1101     types: []const ir.Type,
1102     decoded: []const ir.Attribute,
1103 ) !ir.Attribute {
1104     const kind = try reader.readByte();
1105     switch (kind) {
1106         @backingInt(AttrKind.integer) => {
1107             const value = try reader.readSLEB();
1108             const width = try reader.readByte();
1109             const is_signed = (try reader.readByte()) != 0;
1110             return ctx.getIntegerAttr(value, width, is_signed);
1111         },
1112         @backingInt(AttrKind.float_) => {
1113             const bits = try reader.readU64();
1114             const width = try reader.readByte();
1115             return ctx.getFloatAttr(@bitCast(bits), width);
1116         },
1117         @backingInt(AttrKind.bool_) => {
1118             const value = (try reader.readByte()) != 0;
1119             return ctx.getBoolAttr(value);
1120         },
1121         @backingInt(AttrKind.string) => {
1122             const value = try reader.readBytesWithLen();
1123             return ctx.getStringAttr(value);
1124         },
1125         @backingInt(AttrKind.symbol_ref) => {
1126             const root_reference = try reader.readBytesWithLen();
1127             const count = try reader.readULEBToUsize();
1128             const nested_references = try allocator.alloc([]const u8, count);
1129             defer allocator.free(nested_references);
1130             for (nested_references) |*slot| {
1131                 slot.* = try reader.readBytesWithLen();
1132             }
1133             return ctx.getSymbolRefAttr(root_reference, nested_references);
1134         },
1135         @backingInt(AttrKind.string_list) => {
1136             const count = try reader.readULEBToUsize();
1137             const values = try allocator.alloc([]const u8, count);
1138             defer allocator.free(values);
1139             for (values) |*slot| {
1140                 slot.* = try reader.readBytesWithLen();
1141             }
1142             return ctx.getStringListAttr(values);
1143         },
1144         @backingInt(AttrKind.type_list) => {
1145             const count = try reader.readULEBToUsize();
1146             const values = try allocator.alloc(ir.Type, count);
1147             defer allocator.free(values);
1148             for (values) |*slot| {
1149                 const type_id = try reader.readULEBToUsize();
1150                 if (type_id >= types.len) return error.InvalidTable;
1151                 slot.* = types[type_id];
1152             }
1153             return ctx.getTypeListAttr(values);
1154         },
1155         @backingInt(AttrKind.array) => {
1156             const count = try reader.readULEBToUsize();
1157             const values = try allocator.alloc(ir.Attribute, count);
1158             defer allocator.free(values);
1159             for (values) |*slot| {
1160                 const attr_id = try reader.readULEBToUsize();
1161                 if (attr_id >= decoded.len) return error.InvalidTable;
1162                 slot.* = decoded[attr_id];
1163             }
1164             return ctx.getArrayAttr(values);
1165         },
1166         @backingInt(AttrKind.dialect) => {
1167             const dialect_id = try reader.readULEBToUsize();
1168             const name_id = try reader.readULEBToUsize();
1169             const payload = try reader.readBytesWithLen();
1170             if (dialect_id >= dialects.len) return error.InvalidTable;
1171             if (name_id >= strings.len) return error.InvalidTable;
1172 
1173             const dialect = dialects[dialect_id].name;
1174             const attr_name = strings[name_id];
1175             const transient_allocator = ir.context.transientAllocator(ctx);
1176             const full_name = try std.fmt.allocPrint(transient_allocator, "{s}.{s}", .{ dialect, attr_name });
1177             defer transient_allocator.free(full_name);
1178 
1179             return ctx.getDialectAttr(full_name, payload);
1180         },
1181         else => return error.UnsupportedAttribute,
1182     }
1183 }
1184 
1185 fn decodeLocation(
1186     allocator: std.mem.Allocator,
1187     reader: *SliceReader,
1188     strings: []const []const u8,
1189     locs: []const ir.Location,
1190     loc_slices: *std.ArrayList([]const ir.Location),
1191 ) !ir.Location {
1192     const kind = try reader.readByte();
1193     switch (kind) {
1194         @backingInt(LocationKind.unknown) => return ir.Location.getUnknown(),
1195         @backingInt(LocationKind.file) => {
1196             const name_id = try reader.readULEBToUsize();
1197             const line = try reader.readULEBToU32();
1198             const column = try reader.readULEBToU32();
1199             if (name_id >= strings.len) return error.InvalidTable;
1200             return ir.Location.getFile(strings[name_id], line, column);
1201         },
1202         @backingInt(LocationKind.file_range) => {
1203             const name_id = try reader.readULEBToUsize();
1204             const start_byte = try reader.readULEB();
1205             const start_line = try reader.readULEBToU32();
1206             const start_column = try reader.readULEBToU32();
1207             const end_byte = try reader.readULEB();
1208             const end_line = try reader.readULEBToU32();
1209             const end_column = try reader.readULEBToU32();
1210             if (name_id >= strings.len or start_byte > end_byte) return error.InvalidTable;
1211             return ir.Location.getFileRange(
1212                 strings[name_id],
1213                 .{ .byte = start_byte, .line = start_line, .column = start_column },
1214                 .{ .byte = end_byte, .line = end_line, .column = end_column },
1215             );
1216         },
1217         @backingInt(LocationKind.name) => {
1218             const name_id = try reader.readULEBToUsize();
1219             const has_child = (try reader.readByte()) != 0;
1220             if (name_id >= strings.len) return error.InvalidTable;
1221             if (has_child) {
1222                 const child_id = try reader.readULEBToUsize();
1223                 if (child_id >= locs.len) return error.InvalidTable;
1224                 return ir.Location.getName(strings[name_id], &locs[child_id]);
1225             }
1226             return ir.Location.getName(strings[name_id], null);
1227         },
1228         @backingInt(LocationKind.fused) => {
1229             const count = try reader.readULEBToUsize();
1230             const fused_locs = try allocator.alloc(ir.Location, count);
1231             errdefer allocator.free(fused_locs);
1232             var i: usize = 0;
1233             while (i < count) : (i += 1) {
1234                 const loc_id = try reader.readULEBToUsize();
1235                 if (loc_id >= locs.len) return error.InvalidTable;
1236                 fused_locs[i] = locs[loc_id];
1237             }
1238             try loc_slices.append(allocator, fused_locs);
1239             return .{ .fused = .{ .locations = fused_locs, .metadata = null } };
1240         },
1241         @backingInt(LocationKind.call_site) => {
1242             const callee_id = try reader.readULEBToUsize();
1243             const caller_id = try reader.readULEBToUsize();
1244             if (callee_id >= locs.len or caller_id >= locs.len) return error.InvalidTable;
1245             return .{ .call_site = .{ .callee = &locs[callee_id], .caller = &locs[caller_id] } };
1246         },
1247         else => return error.UnsupportedLocation,
1248     }
1249 }
1250 
1251 fn encodeType(builder: *TableBuilder, writer: *BufferWriter, typ: ir.Type) !void {
1252     const storage = typ.getDialectStorage() orelse return error.UnsupportedType;
1253     if (builtinScalarFromName(storage.name)) |builtin| {
1254         if (storage.param_key.len > 0) return error.UnsupportedType;
1255         try writer.writeByte(@backingInt(TypeKind.builtin_scalar));
1256         try writer.writeByte(@backingInt(builtin.kind));
1257         try writer.writeByte(builtin.width);
1258         return;
1259     }
1260     const dialect = dialectNamespace(storage.name);
1261     if (dialect.len == 0) {
1262         try writer.writeByte(@backingInt(TypeKind.dialect_only));
1263         const dialect_id = try builder.internDialect(storage.name);
1264         try writer.writeULEB(dialect_id);
1265         if (storage.param_key.len > 0) {
1266             try writer.writeByte(1);
1267             try writer.writeULEB(try builder.internString(storage.param_key));
1268         } else {
1269             try writer.writeByte(0);
1270         }
1271         return;
1272     }
1273     try writer.writeByte(@backingInt(TypeKind.dialect));
1274     const dialect_id = try builder.internDialect(dialect);
1275     try writer.writeULEB(dialect_id);
1276     const type_name = dialectTypeName(storage.name);
1277     try writer.writeULEB(try builder.internString(type_name));
1278     if (storage.param_key.len > 0) {
1279         try writer.writeByte(1);
1280         try writer.writeULEB(try builder.internString(storage.param_key));
1281     } else {
1282         try writer.writeByte(0);
1283     }
1284 }
1285 
1286 fn encodeAttribute(builder: *TableBuilder, writer: *BufferWriter, attr: ir.Attribute) !void {
1287     if (attr.abstract.name.len == 0) return error.UnsupportedAttribute;
1288     const full_name = attr.abstract.name;
1289 
1290     if (std.mem.eql(u8, full_name, ir.builtin_attr_names.integer)) {
1291         const int_attr = attr.cast(ir.Attribute.IntegerAttr) orelse return error.UnsupportedAttribute;
1292         try writer.writeByte(@backingInt(AttrKind.integer));
1293         try writer.writeSLEB(int_attr.value);
1294         try writer.writeByte(int_attr.width);
1295         try writer.writeByte(if (int_attr.is_signed) 1 else 0);
1296         return;
1297     } else if (std.mem.eql(u8, full_name, ir.builtin_attr_names.float_)) {
1298         const float_attr = attr.cast(ir.Attribute.FloatAttr) orelse return error.UnsupportedAttribute;
1299         try writer.writeByte(@backingInt(AttrKind.float_));
1300         try writer.writeU64(@bitCast(float_attr.value));
1301         try writer.writeByte(float_attr.width);
1302         return;
1303     } else if (std.mem.eql(u8, full_name, ir.builtin_attr_names.bool_)) {
1304         const bool_attr = attr.cast(ir.Attribute.BoolAttr) orelse return error.UnsupportedAttribute;
1305         try writer.writeByte(@backingInt(AttrKind.bool_));
1306         try writer.writeByte(if (bool_attr.value) 1 else 0);
1307         return;
1308     } else if (std.mem.eql(u8, full_name, ir.builtin_attr_names.string)) {
1309         const str_attr = attr.cast(ir.Attribute.StringAttr) orelse return error.UnsupportedAttribute;
1310         try writer.writeByte(@backingInt(AttrKind.string));
1311         try writer.writeBytes(str_attr.value);
1312         return;
1313     } else if (std.mem.eql(u8, full_name, ir.builtin_attr_names.symbol_ref)) {
1314         const symbol_ref_attr = attr.cast(ir.Attribute.SymbolRefAttr) orelse return error.UnsupportedAttribute;
1315         try writer.writeByte(@backingInt(AttrKind.symbol_ref));
1316         try writer.writeBytes(symbol_ref_attr.root_reference);
1317         try writer.writeULEB(symbol_ref_attr.nested_references.len);
1318         for (symbol_ref_attr.nested_references) |nested| {
1319             try writer.writeBytes(nested);
1320         }
1321         return;
1322     } else if (std.mem.eql(u8, full_name, ir.builtin_attr_names.string_list)) {
1323         const str_list_attr = attr.cast(ir.Attribute.StringListAttr) orelse return error.UnsupportedAttribute;
1324         try writer.writeByte(@backingInt(AttrKind.string_list));
1325         try writer.writeULEB(str_list_attr.values.len);
1326         for (str_list_attr.values) |value| {
1327             try writer.writeBytes(value);
1328         }
1329         return;
1330     } else if (std.mem.eql(u8, full_name, ir.builtin_attr_names.type_list)) {
1331         const type_list_attr = attr.cast(ir.Attribute.TypeListAttr) orelse return error.UnsupportedAttribute;
1332         try writer.writeByte(@backingInt(AttrKind.type_list));
1333         try writer.writeULEB(type_list_attr.values.len);
1334         for (type_list_attr.values) |typ| {
1335             try writer.writeULEB(try lookupTypeId(builder, typ));
1336         }
1337         return;
1338     } else if (std.mem.eql(u8, full_name, ir.builtin_attr_names.array)) {
1339         const array_attr = attr.cast(ir.Attribute.ArrayAttr) orelse return error.UnsupportedAttribute;
1340         try writer.writeByte(@backingInt(AttrKind.array));
1341         try writer.writeULEB(array_attr.values.len);
1342         for (array_attr.values) |child| {
1343             try writer.writeULEB(try lookupAttrId(builder, child));
1344         }
1345         return;
1346     }
1347 
1348     const dialect = dialectNamespace(full_name);
1349     const dialect_id = if (dialect.len > 0)
1350         try builder.internDialect(dialect)
1351     else
1352         0;
1353     const attr_name = dialectTypeName(full_name);
1354     const attr_name_id = try builder.internString(attr_name);
1355 
1356     const dialect_attr = attr.cast(ir.Attribute.DialectAttr) orelse return error.UnsupportedAttribute;
1357 
1358     try writer.writeByte(@backingInt(AttrKind.dialect));
1359     try writer.writeULEB(dialect_id);
1360     try writer.writeULEB(attr_name_id);
1361     try writer.writeBytes(dialect_attr.payload);
1362 }
1363 
1364 fn encodeLocation(builder: *TableBuilder, writer: *BufferWriter, loc: ir.Location) !void {
1365     switch (loc) {
1366         .unknown => {
1367             try writer.writeByte(@backingInt(LocationKind.unknown));
1368         },
1369         .file => |file_loc| {
1370             try writer.writeByte(@backingInt(LocationKind.file));
1371             try writer.writeULEB(try builder.internString(file_loc.filename));
1372             try writer.writeULEB(file_loc.line);
1373             try writer.writeULEB(file_loc.column);
1374         },
1375         .file_range => |range| {
1376             try writer.writeByte(@backingInt(LocationKind.file_range));
1377             try writer.writeULEB(try builder.internString(range.filename));
1378             try writer.writeULEB(range.start.byte);
1379             try writer.writeULEB(range.start.line);
1380             try writer.writeULEB(range.start.column);
1381             try writer.writeULEB(range.end.byte);
1382             try writer.writeULEB(range.end.line);
1383             try writer.writeULEB(range.end.column);
1384         },
1385         .name => |name_loc| {
1386             try writer.writeByte(@backingInt(LocationKind.name));
1387             try writer.writeULEB(try builder.internString(name_loc.name));
1388             if (name_loc.child) |child| {
1389                 try writer.writeByte(1);
1390                 try writer.writeULEB(try builder.internLocation(child.*));
1391             } else {
1392                 try writer.writeByte(0);
1393             }
1394         },
1395         .fused => |fused| {
1396             try writer.writeByte(@backingInt(LocationKind.fused));
1397             try writer.writeULEB(fused.locations.len);
1398             for (fused.locations) |child_loc| {
1399                 try writer.writeULEB(try builder.internLocation(child_loc));
1400             }
1401         },
1402         .call_site => |call_site| {
1403             try writer.writeByte(@backingInt(LocationKind.call_site));
1404             try writer.writeULEB(try builder.internLocation(call_site.callee.*));
1405             try writer.writeULEB(try builder.internLocation(call_site.caller.*));
1406         },
1407     }
1408 }
1409 
1410 fn collectOperationTables(builder: *TableBuilder, op: *ir.Operation) anyerror!void {
1411     _ = try builder.internString(op.name.name);
1412     _ = try builder.internLocation(op.location);
1413 
1414     for (op.results.items) |result| {
1415         _ = try builder.internType(result.type);
1416     }
1417     for (op.operands.items) |operand| {
1418         _ = try builder.internType(operand.value.type);
1419     }
1420     for (op.getRawDictionaryAttrs()) |attr| {
1421         _ = try builder.internString(attr.name);
1422         _ = try builder.internAttribute(attr.value);
1423     }
1424     if (try op.getPropertiesAsAttr()) |properties| {
1425         _ = try builder.internAttribute(properties);
1426     }
1427 
1428     for (op.regions.items) |*region| {
1429         var blocks = region.getBlocks();
1430         while (blocks.next()) |block| {
1431             for (block.arguments.items, 0..) |arg, index| {
1432                 _ = try builder.internType(arg.type);
1433                 _ = try builder.internLocation(block.getArgumentLocation(index).?);
1434             }
1435 
1436             var current: ?*ir.Operation = if (block.operations.head) |any_ptr|
1437                 @ptrCast(@alignCast(any_ptr))
1438             else
1439                 null;
1440             while (current) |child| {
1441                 try collectOperationTables(builder, child);
1442                 current = child.next_op;
1443             }
1444         }
1445     }
1446 }
1447 
1448 fn lookupStringId(builder: *TableBuilder, value: []const u8) !u32 {
1449     return builder.strings.map.get(value) orelse error.MissingTableEntry;
1450 }
1451 
1452 fn lookupTypeId(builder: *TableBuilder, typ: ir.Type) !u32 {
1453     return findTypeIndex(builder.types.items, typ) orelse error.MissingTableEntry;
1454 }
1455 
1456 fn lookupAttrId(builder: *TableBuilder, attr: ir.Attribute) !u32 {
1457     return findAttrIndex(builder.attrs.items, attr) orelse error.MissingTableEntry;
1458 }
1459 
1460 fn lookupLocationId(builder: *TableBuilder, loc: ir.Location) !u32 {
1461     return findLocationIndex(builder.locs.items, loc) orelse error.MissingTableEntry;
1462 }
1463 
1464 const ModuleEncoder = struct {
1465     allocator: std.mem.Allocator,
1466     builder: *TableBuilder,
1467     value_ids: std.AutoHashMap(*ir.Value, u32),
1468     next_value_id: u32,
1469 
1470     pub fn init(allocator: std.mem.Allocator, builder: *TableBuilder) !ModuleEncoder {
1471         return .{
1472             .allocator = allocator,
1473             .builder = builder,
1474             .value_ids = std.AutoHashMap(*ir.Value, u32).init(allocator),
1475             .next_value_id = 0,
1476         };
1477     }
1478 
1479     pub fn deinit(self: *ModuleEncoder) void {
1480         self.value_ids.deinit();
1481     }
1482 
1483     pub fn defineValue(self: *ModuleEncoder, value: *ir.Value) !u32 {
1484         if (self.value_ids.get(value)) |id| return id;
1485         const id = self.next_value_id;
1486         self.next_value_id += 1;
1487         try self.value_ids.put(value, id);
1488         return id;
1489     }
1490 
1491     pub fn getValueId(self: *ModuleEncoder, value: *ir.Value) !u32 {
1492         return self.value_ids.get(value) orelse error.InvalidModule;
1493     }
1494 };
1495 
1496 fn encodeModuleSection(
1497     allocator: std.mem.Allocator,
1498     builder: *TableBuilder,
1499     module: *ir.Operation,
1500 ) ![]u8 {
1501     var encoder = try ModuleEncoder.init(allocator, builder);
1502     defer encoder.deinit();
1503 
1504     var writer = BufferWriter.init(allocator);
1505     errdefer writer.deinit();
1506 
1507     try encodeOperation(&encoder, &writer, module, null);
1508 
1509     return writer.toOwnedSlice();
1510 }
1511 
1512 fn indexRegionBlocks(
1513     encoder: *ModuleEncoder,
1514     region: *ir.Region,
1515     blocks: *std.ArrayList(*ir.Block),
1516     block_ids: *std.AutoHashMap(*ir.Block, u32),
1517 ) anyerror!void {
1518     var iter = region.getBlocks();
1519     while (iter.next()) |block| {
1520         const id: u32 = @intCast(blocks.items.len);
1521         try blocks.append(encoder.allocator, block);
1522         try block_ids.put(block, id);
1523     }
1524 }
1525 
1526 fn encodeBlockArguments(encoder: *ModuleEncoder, writer: *BufferWriter, block: *ir.Block) anyerror!void {
1527     try writer.writeULEB(block.arguments.items.len);
1528     for (block.arguments.items, 0..) |arg, index| {
1529         try writer.writeULEB(try lookupTypeId(encoder.builder, arg.type));
1530         try writer.writeULEB(try lookupLocationId(
1531             encoder.builder,
1532             block.getArgumentLocation(index).?,
1533         ));
1534         _ = try encoder.defineValue(arg);
1535     }
1536 }
1537 
1538 fn countBlockOps(block: *ir.Block) usize {
1539     var count: usize = 0;
1540     var current: ?*ir.Operation = if (block.operations.head) |any_ptr|
1541         @ptrCast(@alignCast(any_ptr))
1542     else
1543         null;
1544     while (current) |op| {
1545         count += 1;
1546         current = op.next_op;
1547     }
1548     return count;
1549 }
1550 
1551 fn encodeOperation(
1552     encoder: *ModuleEncoder,
1553     writer: *BufferWriter,
1554     op: *ir.Operation,
1555     block_ids: ?*const std.AutoHashMap(*ir.Block, u32),
1556 ) anyerror!void {
1557     const name_id = try lookupStringId(encoder.builder, op.name.name);
1558     const loc_id = try lookupLocationId(encoder.builder, op.location);
1559 
1560     try writer.writeULEB(name_id);
1561     try writer.writeULEB(loc_id);
1562 
1563     try writer.writeULEB(op.results.items.len);
1564     for (op.results.items) |result| {
1565         try writer.writeULEB(try lookupTypeId(encoder.builder, result.type));
1566     }
1567 
1568     try writer.writeULEB(op.operands.items.len);
1569     for (op.operands.items) |operand| {
1570         const value_id = try encoder.getValueId(operand.value);
1571         try writer.writeULEB(value_id);
1572     }
1573 
1574     const attrs = op.getRawDictionaryAttrs();
1575     try writer.writeULEB(attrs.len);
1576     for (attrs) |attr| {
1577         const attr_name_id = try lookupStringId(encoder.builder, attr.name);
1578         const attr_id = try lookupAttrId(encoder.builder, attr.value);
1579         try writer.writeULEB(attr_name_id);
1580         try writer.writeULEB(attr_id);
1581     }
1582 
1583     if (try op.getPropertiesAsAttr()) |properties| {
1584         try writer.writeByte(1);
1585         try writer.writeULEB(try lookupAttrId(encoder.builder, properties));
1586     } else {
1587         try writer.writeByte(0);
1588     }
1589 
1590     try writer.writeULEB(op.successors.items.len);
1591     if (op.successors.items.len > 0) {
1592         const map = block_ids orelse return error.InvalidModule;
1593         for (op.successors.items) |succ| {
1594             const block_id = map.get(succ) orelse return error.InvalidModule;
1595             try writer.writeULEB(block_id);
1596         }
1597     }
1598 
1599     try writer.writeULEB(op.regions.items.len);
1600 
1601     for (op.results.items) |*result| {
1602         _ = try encoder.defineValue(result);
1603     }
1604 
1605     for (op.regions.items) |*region| {
1606         var region_blocks: std.ArrayList(*ir.Block) = .empty;
1607         defer region_blocks.deinit(encoder.allocator);
1608 
1609         var region_block_ids = std.AutoHashMap(*ir.Block, u32).init(encoder.allocator);
1610         defer region_block_ids.deinit();
1611 
1612         try indexRegionBlocks(encoder, region, &region_blocks, &region_block_ids);
1613         try writer.writeULEB(region_blocks.items.len);
1614 
1615         for (region_blocks.items) |block| {
1616             try encodeBlockArguments(encoder, writer, block);
1617 
1618             const op_count = countBlockOps(block);
1619             try writer.writeULEB(op_count);
1620 
1621             var current: ?*ir.Operation = if (block.operations.head) |any_ptr|
1622                 @ptrCast(@alignCast(any_ptr))
1623             else
1624                 null;
1625             while (current) |child| {
1626                 try encodeOperation(encoder, writer, child, &region_block_ids);
1627                 current = child.next_op;
1628             }
1629         }
1630     }
1631 }
1632 
1633 const ModuleDecoder = struct {
1634     allocator: std.mem.Allocator,
1635     ctx: *ir.Context,
1636     strings: []const []const u8,
1637     types: []const ir.Type,
1638     attrs: []const ir.Attribute,
1639     locs: []const ir.Location,
1640     values: std.ArrayList(*ir.Value),
1641     value_base: usize,
1642 
1643     pub fn init(allocator: std.mem.Allocator, ctx: *ir.Context, tables: *DecodedTables) ModuleDecoder {
1644         return .{
1645             .allocator = allocator,
1646             .ctx = ctx,
1647             .strings = tables.strings,
1648             .types = tables.types,
1649             .attrs = tables.attrs,
1650             .locs = tables.locs,
1651             .values = .empty,
1652             .value_base = 0,
1653         };
1654     }
1655 
1656     pub fn deinit(self: *ModuleDecoder) void {
1657         self.values.deinit(self.allocator);
1658     }
1659 };
1660 
1661 const FlatModuleEnvelope = struct {
1662     name: []const u8,
1663     location: ir.Location,
1664     operation_count: usize,
1665 };
1666 
1667 fn decodeFlatModuleEnvelope(
1668     decoder: *ModuleDecoder,
1669     reader: *SliceReader,
1670 ) !FlatModuleEnvelope {
1671     const name_id = try reader.readULEBToUsize();
1672     const loc_id = try reader.readULEBToUsize();
1673     if (name_id >= decoder.strings.len or loc_id >= decoder.locs.len) {
1674         return error.InvalidTable;
1675     }
1676     if (try reader.readULEBToUsize() != 0) return error.InvalidModule;
1677     if (try reader.readULEBToUsize() != 0) return error.InvalidModule;
1678 
1679     const attr_count = try reader.readULEBToUsize();
1680     var attr_index: usize = 0;
1681     while (attr_index < attr_count) : (attr_index += 1) {
1682         const attr_name_id = try reader.readULEBToUsize();
1683         const attr_id = try reader.readULEBToUsize();
1684         if (attr_name_id >= decoder.strings.len or
1685             attr_id >= decoder.attrs.len)
1686         {
1687             return error.InvalidTable;
1688         }
1689     }
1690 
1691     const has_properties = try reader.readByte();
1692     if (has_properties > 1) return error.InvalidModule;
1693     if (has_properties == 1) {
1694         const property_id = try reader.readULEBToUsize();
1695         if (property_id >= decoder.attrs.len) return error.InvalidTable;
1696     }
1697     if (try reader.readULEBToUsize() != 0) return error.InvalidModule;
1698     if (try reader.readULEBToUsize() != 1) return error.InvalidModule;
1699     if (try reader.readULEBToUsize() != 1) return error.InvalidModule;
1700     if (try reader.readULEBToUsize() != 0) return error.InvalidModule;
1701 
1702     return .{
1703         .name = decoder.strings[name_id],
1704         .location = decoder.locs[loc_id],
1705         .operation_count = try reader.readULEBToUsize(),
1706     };
1707 }
1708 
1709 fn decodeModuleSection(
1710     allocator: std.mem.Allocator,
1711     ctx: *ir.Context,
1712     bytes: []const u8,
1713     tables: *DecodedTables,
1714 ) !*ir.Operation {
1715     var decoder = ModuleDecoder.init(allocator, ctx, tables);
1716     defer decoder.deinit();
1717 
1718     var reader = SliceReader.init(bytes);
1719     const module_op = try decodeOperation(&decoder, &reader, null, null);
1720     errdefer module_op.erase();
1721     if (reader.offset != bytes.len) return error.InvalidModule;
1722     return module_op;
1723 }
1724 
1725 fn createRegionBlocks(decoder: *ModuleDecoder, reader: *SliceReader, region: *ir.Region) anyerror!std.ArrayList(*ir.Block) {
1726     const block_count = try reader.readULEBToUsize();
1727 
1728     var blocks: std.ArrayList(*ir.Block) = .empty;
1729     errdefer blocks.deinit(decoder.allocator);
1730 
1731     var i: usize = 0;
1732     while (i < block_count) : (i += 1) {
1733         const block = try region.addBlock();
1734         try blocks.append(decoder.allocator, block);
1735     }
1736 
1737     return blocks;
1738 }
1739 
1740 fn decodeBlockArguments(decoder: *ModuleDecoder, reader: *SliceReader, block: *ir.Block) anyerror!void {
1741     const arg_count = try reader.readULEBToUsize();
1742     if (arg_count > 0) {
1743         try block.arguments.ensureTotalCapacity(block.allocator, arg_count);
1744     }
1745     const arg_start = block.arguments.items.len;
1746     var arg_idx: usize = 0;
1747     while (arg_idx < arg_count) : (arg_idx += 1) {
1748         const type_id = try reader.readULEBToUsize();
1749         const loc_id = try reader.readULEBToUsize();
1750         if (type_id >= decoder.types.len or loc_id >= decoder.locs.len) return error.InvalidTable;
1751         _ = try block.addArgument(decoder.types[type_id], decoder.locs[loc_id]);
1752     }
1753     for (block.arguments.items[arg_start..]) |arg| {
1754         try decoder.values.append(decoder.allocator, arg);
1755     }
1756 }
1757 
1758 fn decodeOperation(
1759     decoder: *ModuleDecoder,
1760     reader: *SliceReader,
1761     blocks: ?[]const *ir.Block,
1762     parent_block: ?*ir.Block,
1763 ) anyerror!*ir.Operation {
1764     const name_id = try reader.readULEBToUsize();
1765     const loc_id = try reader.readULEBToUsize();
1766     if (name_id >= decoder.strings.len or loc_id >= decoder.locs.len) return error.InvalidTable;
1767 
1768     const result_count = try reader.readULEBToUsize();
1769     var result_types: []ir.Type = &.{};
1770     if (result_count > 0) {
1771         result_types = try reader.readTypeList(decoder.allocator, result_count, decoder.types);
1772     }
1773     defer if (result_types.len > 0) decoder.allocator.free(result_types);
1774 
1775     const operand_count = try reader.readULEBToUsize();
1776     var operands: []*ir.Value = &.{};
1777     defer if (operands.len > 0) decoder.allocator.free(operands);
1778     if (operand_count > 0) {
1779         operands = try decoder.allocator.alloc(*ir.Value, operand_count);
1780         var op_idx: usize = 0;
1781         while (op_idx < operand_count) : (op_idx += 1) {
1782             const value_id = try reader.readULEBToUsize();
1783             if (value_id < decoder.value_base) return error.InvalidModule;
1784             const local_value_id = value_id - decoder.value_base;
1785             if (local_value_id >= decoder.values.items.len) {
1786                 return error.InvalidModule;
1787             }
1788             operands[op_idx] = decoder.values.items[local_value_id];
1789         }
1790     }
1791     const attr_count = try reader.readULEBToUsize();
1792     var attrs: []ir.NamedAttribute = &.{};
1793     defer if (attrs.len > 0) decoder.allocator.free(attrs);
1794     if (attr_count > 0) {
1795         attrs = try decoder.allocator.alloc(ir.NamedAttribute, attr_count);
1796         var attr_idx: usize = 0;
1797         while (attr_idx < attr_count) : (attr_idx += 1) {
1798             const attr_name_id = try reader.readULEBToUsize();
1799             const attr_id = try reader.readULEBToUsize();
1800             if (attr_name_id >= decoder.strings.len or attr_id >= decoder.attrs.len) return error.InvalidTable;
1801             attrs[attr_idx] = .{
1802                 .name = decoder.strings[attr_name_id],
1803                 .value = decoder.attrs[attr_id],
1804             };
1805         }
1806     }
1807     const has_properties = try reader.readByte();
1808     if (has_properties > 1) return error.InvalidModule;
1809     const properties = if (has_properties == 1) blk: {
1810         const property_id = try reader.readULEBToUsize();
1811         if (property_id >= decoder.attrs.len) return error.InvalidTable;
1812         break :blk decoder.attrs[property_id];
1813     } else null;
1814 
1815     const succ_count = try reader.readULEBToUsize();
1816     var successors: []*ir.Block = &.{};
1817     defer if (successors.len > 0) decoder.allocator.free(successors);
1818     if (succ_count > 0) {
1819         const block_list = blocks orelse return error.InvalidModule;
1820         successors = try decoder.allocator.alloc(*ir.Block, succ_count);
1821         var succ_idx: usize = 0;
1822         while (succ_idx < succ_count) : (succ_idx += 1) {
1823             const block_id = try reader.readULEBToUsize();
1824             if (block_id >= block_list.len) return error.InvalidModule;
1825             successors[succ_idx] = block_list[block_id];
1826         }
1827     }
1828     const region_count = try reader.readULEBToUsize();
1829 
1830     var state = ir.Operation.State.init(decoder.strings[name_id], decoder.locs[loc_id]);
1831     if (operand_count > 0) state.addOperands(operands);
1832     if (result_count > 0) state.addTypes(result_types);
1833     if (attr_count > 0) state.addRawAttributes(attrs);
1834     if (properties) |payload| try state.setPropertiesAttr(payload);
1835     if (succ_count > 0) state.addSuccessors(successors);
1836     var region_idx: usize = 0;
1837     while (region_idx < region_count) : (region_idx += 1) {
1838         state.addRegion();
1839     }
1840 
1841     const op = try decoder.ctx.createOperation(state);
1842     errdefer op.erase();
1843     if (parent_block) |block| {
1844         try block.addOperation(op);
1845     }
1846 
1847     for (op.results.items) |*result| {
1848         try decoder.values.append(decoder.allocator, result);
1849     }
1850 
1851     region_idx = 0;
1852     while (region_idx < region_count) : (region_idx += 1) {
1853         const region = op.getRegion(region_idx) orelse return error.InvalidModule;
1854         var region_blocks = try createRegionBlocks(decoder, reader, region);
1855         defer region_blocks.deinit(decoder.allocator);
1856 
1857         for (region_blocks.items) |block| {
1858             try decodeBlockArguments(decoder, reader, block);
1859 
1860             const op_count = try reader.readULEBToUsize();
1861             var op_idx: usize = 0;
1862             while (op_idx < op_count) : (op_idx += 1) {
1863                 _ = try decodeOperation(decoder, reader, region_blocks.items, block);
1864             }
1865         }
1866     }
1867 
1868     return op;
1869 }
1870 
1871 pub const TypeKind = enum(u8) {
1872     dialect = 0,
1873     builtin_scalar = 1,
1874     dialect_only = 2,
1875 };
1876 
1877 pub const BuiltinScalarKind = enum(u8) {
1878     index = 0,
1879     bool_ = 1,
1880     integer = 2,
1881     float_ = 3,
1882 };
1883 
1884 fn builtinScalarFromName(name: []const u8) ?struct { kind: BuiltinScalarKind, width: u8 } {
1885     if (std.mem.eql(u8, name, "arith.index")) return .{ .kind = .index, .width = 0 };
1886     if (std.mem.eql(u8, name, "arith.bool")) return .{ .kind = .bool_, .width = 1 };
1887     if (std.mem.eql(u8, name, "arith.i8")) return .{ .kind = .integer, .width = 8 };
1888     if (std.mem.eql(u8, name, "arith.i16")) return .{ .kind = .integer, .width = 16 };
1889     if (std.mem.eql(u8, name, "arith.i32")) return .{ .kind = .integer, .width = 32 };
1890     if (std.mem.eql(u8, name, "arith.i64")) return .{ .kind = .integer, .width = 64 };
1891     if (std.mem.eql(u8, name, "arith.f16")) return .{ .kind = .float_, .width = 16 };
1892     if (std.mem.eql(u8, name, "arith.f32")) return .{ .kind = .float_, .width = 32 };
1893     if (std.mem.eql(u8, name, "arith.f64")) return .{ .kind = .float_, .width = 64 };
1894     return null;
1895 }
1896 
1897 fn builtinScalarName(kind: BuiltinScalarKind, width: u8) ?[]const u8 {
1898     return switch (kind) {
1899         .index => "arith.index",
1900         .bool_ => "arith.bool",
1901         .integer => switch (width) {
1902             8 => "arith.i8",
1903             16 => "arith.i16",
1904             32 => "arith.i32",
1905             64 => "arith.i64",
1906             else => null,
1907         },
1908         .float_ => switch (width) {
1909             16 => "arith.f16",
1910             32 => "arith.f32",
1911             64 => "arith.f64",
1912             else => null,
1913         },
1914     };
1915 }
1916 
1917 pub const AttrKind = enum(u8) {
1918     dialect = 0,
1919     integer = 1,
1920     float_ = 2,
1921     bool_ = 3,
1922     string = 4,
1923     type_list = 5,
1924     string_list = 6,
1925     symbol_ref = 7,
1926     array = 8,
1927 };
1928 
1929 pub const LocationKind = enum(u8) {
1930     unknown = 0,
1931     file = 1,
1932     name = 2,
1933     fused = 3,
1934     call_site = 4,
1935     file_range = 5,
1936 };
1937 
1938 fn findTypeIndex(list: []const ir.Type, typ: ir.Type) ?u32 {
1939     for (list, 0..) |item, idx| {
1940         if (item.eql(typ)) return @intCast(idx);
1941     }
1942     return null;
1943 }
1944 
1945 fn findAttrIndex(list: []const ir.Attribute, attr: ir.Attribute) ?u32 {
1946     for (list, 0..) |item, idx| {
1947         if (attrEqual(item, attr)) return @intCast(idx);
1948     }
1949     return null;
1950 }
1951 
1952 fn findLocationIndex(list: []const ir.Location, loc: ir.Location) ?u32 {
1953     for (list, 0..) |item, idx| {
1954         if (locationEqual(item, loc)) return @intCast(idx);
1955     }
1956     return null;
1957 }
1958 
1959 fn attrEqual(a: ir.Attribute, b: ir.Attribute) bool {
1960     return a.eql(b);
1961 }
1962 
1963 fn locationEqual(a: ir.Location, b: ir.Location) bool {
1964     if (std.meta.activeTag(a) != std.meta.activeTag(b)) return false;
1965     switch (a) {
1966         .unknown => return true,
1967         .file => |lhs| {
1968             const rhs = b.file;
1969             return lhs.line == rhs.line and lhs.column == rhs.column and std.mem.eql(u8, lhs.filename, rhs.filename);
1970         },
1971         .file_range => |lhs| {
1972             const rhs = b.file_range;
1973             return lhs.start.byte == rhs.start.byte and
1974                 lhs.start.line == rhs.start.line and
1975                 lhs.start.column == rhs.start.column and
1976                 lhs.end.byte == rhs.end.byte and
1977                 lhs.end.line == rhs.end.line and
1978                 lhs.end.column == rhs.end.column and
1979                 std.mem.eql(u8, lhs.filename, rhs.filename);
1980         },
1981         .name => |lhs| {
1982             const rhs = b.name;
1983             if (!std.mem.eql(u8, lhs.name, rhs.name)) return false;
1984             if (lhs.child == null and rhs.child == null) return true;
1985             if (lhs.child == null or rhs.child == null) return false;
1986             return locationEqual(lhs.child.?.*, rhs.child.?.*);
1987         },
1988         .fused => |lhs| {
1989             const rhs = b.fused;
1990             if (lhs.locations.len != rhs.locations.len) return false;
1991             for (lhs.locations, 0..) |loc, idx| {
1992                 if (!locationEqual(loc, rhs.locations[idx])) return false;
1993             }
1994             return true;
1995         },
1996         .call_site => |lhs| {
1997             const rhs = b.call_site;
1998             return locationEqual(lhs.callee.*, rhs.callee.*) and locationEqual(lhs.caller.*, rhs.caller.*);
1999         },
2000     }
2001 }
2002 
2003 fn dialectNamespace(full_name: []const u8) []const u8 {
2004     const op_name = ir.Operation.OperationName.init(full_name);
2005     return op_name.getDialectNamespace();
2006 }
2007 
2008 fn dialectTypeName(full_name: []const u8) []const u8 {
2009     if (std.mem.indexOf(u8, full_name, ".")) |idx| {
2010         return full_name[idx + 1 ..];
2011     }
2012     return full_name;
2013 }
2014 
2015 const BufferWriter = struct {
2016     allocator: std.mem.Allocator,
2017     buffer: std.ArrayList(u8),
2018 
2019     pub fn init(allocator: std.mem.Allocator) BufferWriter {
2020         return .{ .allocator = allocator, .buffer = .empty };
2021     }
2022 
2023     pub fn deinit(self: *BufferWriter) void {
2024         self.buffer.deinit(self.allocator);
2025     }
2026 
2027     pub fn toOwnedSlice(self: *BufferWriter) ![]u8 {
2028         return self.buffer.toOwnedSlice(self.allocator);
2029     }
2030 
2031     pub fn writeAll(self: *BufferWriter, bytes: []const u8) !void {
2032         try self.buffer.appendSlice(self.allocator, bytes);
2033     }
2034 
2035     pub fn writeByte(self: *BufferWriter, value: u8) !void {
2036         try self.buffer.append(self.allocator, value);
2037     }
2038 
2039     pub fn writeU16(self: *BufferWriter, value: u16) !void {
2040         var buf: [2]u8 = undefined;
2041         std.mem.writeInt(u16, &buf, value, .little);
2042         try self.writeAll(buf[0..]);
2043     }
2044 
2045     pub fn writeU32(self: *BufferWriter, value: u32) !void {
2046         var buf: [4]u8 = undefined;
2047         std.mem.writeInt(u32, &buf, value, .little);
2048         try self.writeAll(buf[0..]);
2049     }
2050 
2051     pub fn writeU64(self: *BufferWriter, value: u64) !void {
2052         var buf: [8]u8 = undefined;
2053         std.mem.writeInt(u64, &buf, value, .little);
2054         try self.writeAll(buf[0..]);
2055     }
2056 
2057     pub fn writeULEB(self: *BufferWriter, value: anytype) !void {
2058         const v64: u64 = @intCast(value);
2059         var v = v64;
2060         while (true) {
2061             var byte: u8 = @intCast(v & 0x7f);
2062             v >>= 7;
2063             if (v != 0) byte |= 0x80;
2064             try self.writeByte(byte);
2065             if (v == 0) break;
2066         }
2067     }
2068 
2069     pub fn writeSLEB(self: *BufferWriter, value: i64) !void {
2070         var more = true;
2071         var v = value;
2072         while (more) {
2073             var byte: u8 = @intCast(v & 0x7f);
2074             const sign_bit = (byte & 0x40) != 0;
2075             v >>= 7;
2076             if ((v == 0 and !sign_bit) or (v == -1 and sign_bit)) {
2077                 more = false;
2078             } else {
2079                 byte |= 0x80;
2080             }
2081             try self.writeByte(byte);
2082         }
2083     }
2084 
2085     pub fn writeBytes(self: *BufferWriter, bytes: []const u8) !void {
2086         try self.writeULEB(bytes.len);
2087         try self.writeAll(bytes);
2088     }
2089 };
2090 
2091 pub const SliceReader = struct {
2092     bytes: []const u8,
2093     offset: usize,
2094 
2095     pub fn init(bytes: []const u8) SliceReader {
2096         return .{ .bytes = bytes, .offset = 0 };
2097     }
2098 
2099     pub fn readByte(self: *SliceReader) !u8 {
2100         if (self.offset >= self.bytes.len) return error.EndOfStream;
2101         const value = self.bytes[self.offset];
2102         self.offset += 1;
2103         return value;
2104     }
2105 
2106     pub fn readBytes(self: *SliceReader, len: usize) ![]const u8 {
2107         if (len > self.bytes.len - self.offset) return error.EndOfStream;
2108         const end = self.offset + len;
2109         const slice = self.bytes[self.offset..end];
2110         self.offset = end;
2111         return slice;
2112     }
2113 
2114     pub fn readBytesWithLen(self: *SliceReader) ![]const u8 {
2115         const len = try self.readULEBToUsize();
2116         return self.readBytes(len);
2117     }
2118 
2119     pub fn readU16(self: *SliceReader) !u16 {
2120         const slice = try self.readBytes(2);
2121         var buf: [2]u8 = undefined;
2122         std.mem.copyForwards(u8, buf[0..], slice);
2123         return std.mem.readInt(u16, &buf, .little);
2124     }
2125 
2126     pub fn readU32(self: *SliceReader) !u32 {
2127         const slice = try self.readBytes(4);
2128         var buf: [4]u8 = undefined;
2129         std.mem.copyForwards(u8, buf[0..], slice);
2130         return std.mem.readInt(u32, &buf, .little);
2131     }
2132 
2133     pub fn readU64(self: *SliceReader) !u64 {
2134         const slice = try self.readBytes(8);
2135         var buf: [8]u8 = undefined;
2136         std.mem.copyForwards(u8, buf[0..], slice);
2137         return std.mem.readInt(u64, &buf, .little);
2138     }
2139 
2140     pub fn readULEB(self: *SliceReader) !u64 {
2141         var result: u64 = 0;
2142         var shift: u8 = 0;
2143         while (true) {
2144             const byte = try self.readByte();
2145             const payload = byte & 0x7f;
2146             if (shift == 63) {
2147                 if (payload > 1) return error.InvalidLEB128;
2148                 result |= @as(u64, payload) << 63;
2149             } else {
2150                 const bit_shift: u6 = @intCast(shift);
2151                 result |= @as(u64, payload) << bit_shift;
2152             }
2153             if ((byte & 0x80) == 0) return result;
2154             if (shift >= 63) return error.InvalidLEB128;
2155             shift += 7;
2156         }
2157     }
2158 
2159     pub fn readSLEB(self: *SliceReader) !i64 {
2160         var result: u64 = 0;
2161         var shift: u8 = 0;
2162         while (true) {
2163             const byte = try self.readByte();
2164             const payload = byte & 0x7f;
2165             if (shift == 63) {
2166                 if (payload == 0x7f) {
2167                     result |= @as(u64, 1) << 63;
2168                 } else if (payload != 0) {
2169                     return error.InvalidLEB128;
2170                 }
2171             } else {
2172                 const bit_shift: u6 = @intCast(shift);
2173                 result |= @as(u64, payload) << bit_shift;
2174             }
2175             if ((byte & 0x80) == 0) {
2176                 const used = shift + 7;
2177                 if (used < 64 and (byte & 0x40) != 0) {
2178                     const sign_shift: u6 = @intCast(used);
2179                     result |= @as(u64, std.math.maxInt(u64)) << sign_shift;
2180                 }
2181                 return @bitCast(result);
2182             }
2183             if (shift >= 63) return error.InvalidLEB128;
2184             shift += 7;
2185         }
2186     }
2187 
2188     pub fn readULEBToU32(self: *SliceReader) !u32 {
2189         const value = try self.readULEB();
2190         if (value > std.math.maxInt(u32)) return error.InvalidTable;
2191         return @intCast(value);
2192     }
2193 
2194     pub fn readULEBToUsize(self: *SliceReader) !usize {
2195         const value = try self.readULEB();
2196         if (value > std.math.maxInt(usize)) return error.InvalidTable;
2197         return @intCast(value);
2198     }
2199 
2200     pub fn readTypeList(
2201         self: *SliceReader,
2202         allocator: std.mem.Allocator,
2203         count: usize,
2204         types: []const ir.Type,
2205     ) ![]ir.Type {
2206         const list = try allocator.alloc(ir.Type, count);
2207         errdefer allocator.free(list);
2208         var i: usize = 0;
2209         while (i < count) : (i += 1) {
2210             const type_id = try self.readULEBToUsize();
2211             if (type_id >= types.len) return error.InvalidTable;
2212             list[i] = types[type_id];
2213         }
2214         return list;
2215     }
2216 };
2217 
2218 const BytecodeProperties = struct {
2219     payload: ?ir.Attribute = null,
2220 
2221     fn from(storage: *anyopaque) *@This() {
2222         return @ptrCast(@alignCast(storage));
2223     }
2224 
2225     fn fromConst(storage: *const anyopaque) *const @This() {
2226         return @ptrCast(@alignCast(storage));
2227     }
2228 
2229     fn init(storage: *anyopaque, _: std.mem.Allocator) anyerror!void {
2230         from(storage).* = .{};
2231     }
2232 
2233     fn deinit(_: *anyopaque, _: std.mem.Allocator) void {}
2234 
2235     fn get(_: *const ir.Operation, storage: *const anyopaque, name: []const u8) ?ir.Attribute {
2236         const payload = fromConst(storage).payload orelse return null;
2237         const values = payload.cast(ir.Attribute.ArrayAttr).?.values;
2238         if (std.mem.eql(u8, name, "left")) return values[0];
2239         if (std.mem.eql(u8, name, "right")) return values[1];
2240         return null;
2241     }
2242 
2243     fn getProperties(_: *const ir.Operation, storage: *const anyopaque) ?ir.Attribute {
2244         return fromConst(storage).payload;
2245     }
2246 
2247     fn setProperties(_: *ir.Operation, storage: *anyopaque, attr: ir.Attribute) anyerror!void {
2248         const array = attr.cast(ir.Attribute.ArrayAttr) orelse return error.InvalidTestProperties;
2249         if (array.values.len != 2) return error.InvalidTestProperties;
2250         from(storage).payload = attr;
2251     }
2252 
2253     fn copyProperties(dest: *anyopaque, source: *const anyopaque) anyerror!void {
2254         from(dest).* = fromConst(source).*;
2255     }
2256 
2257     const model = ir.OperationPropertiesModel{
2258         .name = "test.bytecode.properties",
2259         .size = @sizeOf(@This()),
2260         .alignment = std.mem.Alignment.fromByteUnits(@alignOf(@This())),
2261         .init = init,
2262         .deinit = deinit,
2263         .getInherentAttr = get,
2264         .getPropertiesAsAttr = getProperties,
2265         .setPropertiesFromAttr = setProperties,
2266         .copyProperties = copyProperties,
2267     };
2268 };
2269 
2270 fn registerBytecodeProperties(context: *ir.Context) !void {
2271     try context.allowUnregistered();
2272     _ = try context.registerOperation("test.bytecode_properties", .{});
2273     try context.registerOperationInherentAttributeNames(
2274         "test.bytecode_properties",
2275         &.{ "left", "right" },
2276     );
2277     try context.registerOperationPropertiesModel(
2278         "test.bytecode_properties",
2279         BytecodeProperties.model,
2280     );
2281 }
2282 
2283 test "bytecode qualification refuses unqualified property models despite conversion hooks" {
2284     const qualification = @import("root.zig").qualification;
2285     const allocator = std.testing.allocator;
2286     var source_context = try ir.Context.init(allocator, ir.Context.Limits.testing);
2287     defer source_context.deinit(allocator);
2288     try registerBytecodeProperties(&source_context);
2289     var decode_context = try ir.Context.init(allocator, ir.Context.Limits.testing);
2290     defer decode_context.deinit(allocator);
2291     try registerBytecodeProperties(&decode_context);
2292     const left = try source_context.getI64Attr(11);
2293     const right = try source_context.getI64Attr(22);
2294     const payload = try source_context.getArrayAttr(&.{ left, right });
2295     var state = ir.Operation.State.init("test.bytecode_properties", .unknown);
2296     try state.setPropertiesAttr(payload);
2297     const source = try source_context.createOperation(state);
2298     try std.testing.expectError(error.UnencodableProduct, qualification.encode(
2299         allocator,
2300         source,
2301         &.{},
2302         &decode_context,
2303         .{ .operations = 10, .entities = 10, .fields = 100, .depth = 16 },
2304     ));
2305 }
2306 
2307 test "bytecode tables round-trip" {
2308     const testing = std.testing;
2309     const allocator = testing.allocator;
2310     const test_dialect = @import("../dialects/fixture/root.zig");
2311 
2312     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2313     defer ctx.deinit(allocator);
2314     try ctx.allowUnregistered();
2315 
2316     var builder = TableBuilder.init(allocator);
2317     defer builder.deinit();
2318 
2319     const i32_type = try test_dialect.TestDialect.getI32Type(&ctx);
2320     const f64_type = try test_dialect.TestDialect.getF64Type(&ctx);
2321     const dialect_type = try ctx.getDialectTypeFromNameWithKey("test.ty", "key");
2322 
2323     _ = try builder.internType(i32_type);
2324     _ = try builder.internType(f64_type);
2325     _ = try builder.internType(dialect_type);
2326 
2327     const unit_attr = try test_dialect.TestDialect.getUnitAttr(&ctx);
2328     const bool_attr = try test_dialect.TestDialect.getBoolAttr(&ctx, true);
2329     const int_attr = try test_dialect.TestDialect.getIntegerAttr(&ctx, 42);
2330     const string_attr = try test_dialect.TestDialect.getStringAttr(&ctx, "hello");
2331     const symbol_ref_attr = try ctx.getSymbolRefAttr("module", &.{ "nested", "leaf" });
2332     const string_list_attr = try ctx.getStringListAttr(&.{ "ctx", "value_out" });
2333     const type_attr = try test_dialect.TestDialect.getTypeAttr(&ctx, i32_type);
2334     const type_list_attr = try ctx.getTypeListAttr(&.{ i32_type, dialect_type });
2335     const array_attr = try ctx.getArrayAttr(&.{ string_attr, symbol_ref_attr, type_list_attr });
2336 
2337     _ = try builder.internAttribute(unit_attr);
2338     _ = try builder.internAttribute(bool_attr);
2339     _ = try builder.internAttribute(int_attr);
2340     _ = try builder.internAttribute(string_attr);
2341     _ = try builder.internAttribute(symbol_ref_attr);
2342     _ = try builder.internAttribute(string_list_attr);
2343     _ = try builder.internAttribute(type_attr);
2344     _ = try builder.internAttribute(type_list_attr);
2345     _ = try builder.internAttribute(array_attr);
2346 
2347     var child_loc = ir.Location.getFile("test.ir", 3, 9);
2348     const range_loc = ir.Location.getFileRange(
2349         "model.gen",
2350         .{ .byte = 4, .line = 2, .column = 3 },
2351         .{ .byte = 9, .line = 2, .column = 8 },
2352     );
2353     const name_loc = ir.Location.getName("generated", &child_loc);
2354     const unknown_loc = ir.Location.getUnknown();
2355 
2356     _ = try builder.internLocation(name_loc);
2357     _ = try builder.internLocation(unknown_loc);
2358     _ = try builder.internLocation(range_loc);
2359 
2360     const bytes = try encodeTables(allocator, &builder);
2361     defer allocator.free(bytes);
2362 
2363     var decode_ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2364     defer decode_ctx.deinit(allocator);
2365     try decode_ctx.allowUnregistered();
2366 
2367     var tables = try decodeTables(allocator, &decode_ctx, bytes);
2368     defer tables.deinit();
2369 
2370     try testing.expect(tables.format.readable());
2371     try testing.expectEqual(format_version, tables.format.version);
2372     try testing.expectEqual(@as(usize, 2), tables.dialects.len);
2373     try testing.expect(hasDialect(tables.dialects, "builtin"));
2374     try testing.expect(hasDialect(tables.dialects, "test"));
2375     for (tables.dialects) |dialect| {
2376         try testing.expect(dialect.usesDefaultVersion());
2377     }
2378 
2379     const decoded_i32 = try test_dialect.TestDialect.getI32Type(&decode_ctx);
2380     const decoded_f64 = try test_dialect.TestDialect.getF64Type(&decode_ctx);
2381     const decoded_dialect = try decode_ctx.getDialectTypeFromNameWithKey("test.ty", "key");
2382 
2383     try testing.expect(hasType(tables.types, decoded_i32));
2384     try testing.expect(hasType(tables.types, decoded_f64));
2385     try testing.expect(hasType(tables.types, decoded_dialect));
2386 
2387     const decoded_unit = try test_dialect.TestDialect.getUnitAttr(&decode_ctx);
2388     const decoded_bool = try test_dialect.TestDialect.getBoolAttr(&decode_ctx, true);
2389     const decoded_int = try test_dialect.TestDialect.getIntegerAttr(&decode_ctx, 42);
2390     const decoded_string = try test_dialect.TestDialect.getStringAttr(&decode_ctx, "hello");
2391     const decoded_symbol_ref = try decode_ctx.getSymbolRefAttr("module", &.{ "nested", "leaf" });
2392     const decoded_string_list = try decode_ctx.getStringListAttr(&.{ "ctx", "value_out" });
2393     const decoded_type = try test_dialect.TestDialect.getTypeAttr(&decode_ctx, decoded_i32);
2394     const decoded_type_list = try decode_ctx.getTypeListAttr(&.{ decoded_i32, decoded_dialect });
2395     const decoded_array = try decode_ctx.getArrayAttr(&.{ decoded_string, decoded_symbol_ref, decoded_type_list });
2396 
2397     try testing.expect(hasAttr(tables.attrs, decoded_unit));
2398     try testing.expect(hasAttr(tables.attrs, decoded_bool));
2399     try testing.expect(hasAttr(tables.attrs, decoded_int));
2400     try testing.expect(hasAttr(tables.attrs, decoded_string));
2401     try testing.expect(hasAttr(tables.attrs, decoded_symbol_ref));
2402     try testing.expect(hasAttr(tables.attrs, decoded_string_list));
2403     try testing.expect(hasAttr(tables.attrs, decoded_type));
2404     try testing.expect(hasAttr(tables.attrs, decoded_type_list));
2405     try testing.expect(hasAttr(tables.attrs, decoded_array));
2406 
2407     try testing.expect(hasLocation(tables.locs, name_loc));
2408     try testing.expect(hasLocation(tables.locs, unknown_loc));
2409     try testing.expect(hasLocation(tables.locs, range_loc));
2410 }
2411 
2412 test "bytecode round-trips builtin scalar types" {
2413     const testing = std.testing;
2414     const allocator = testing.allocator;
2415     const arith = @import("../dialects/root.zig").ArithDialect;
2416 
2417     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2418     defer ctx.deinit(allocator);
2419 
2420     var builder = TableBuilder.init(allocator);
2421     defer builder.deinit();
2422 
2423     const i32_type = try arith.getI32Type(&ctx);
2424     const f32_type = try arith.getScalarType(&ctx, .f32);
2425     const index_type = try arith.getIndexType(&ctx);
2426     const bool_type = try arith.getScalarType(&ctx, .bool);
2427 
2428     _ = try builder.internType(i32_type);
2429     _ = try builder.internType(f32_type);
2430     _ = try builder.internType(index_type);
2431     _ = try builder.internType(bool_type);
2432 
2433     const bytes = try encodeTables(allocator, &builder);
2434     defer allocator.free(bytes);
2435 
2436     var decode_ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2437     defer decode_ctx.deinit(allocator);
2438     _ = try arith.getI32Type(&decode_ctx);
2439 
2440     var tables = try decodeTables(allocator, &decode_ctx, bytes);
2441     defer tables.deinit();
2442 
2443     const decoded_i32 = try arith.getI32Type(&decode_ctx);
2444     const decoded_f32 = try arith.getScalarType(&decode_ctx, .f32);
2445     const decoded_index = try arith.getIndexType(&decode_ctx);
2446     const decoded_bool = try arith.getScalarType(&decode_ctx, .bool);
2447 
2448     try testing.expect(hasType(tables.types, decoded_i32));
2449     try testing.expect(hasType(tables.types, decoded_f32));
2450     try testing.expect(hasType(tables.types, decoded_index));
2451     try testing.expect(hasType(tables.types, decoded_bool));
2452 }
2453 
2454 fn testTableContainer(allocator: std.mem.Allocator, replacement: ?SectionData) ![]u8 {
2455     var sections = [_]SectionData{
2456         .{ .kind = @backingInt(SectionKind.strings), .flags = 0, .data = "\x02\x01s\x01t" },
2457         .{ .kind = @backingInt(SectionKind.dialects), .flags = 0, .data = "\x01\x00\x00\x00" },
2458         .{ .kind = @backingInt(SectionKind.types), .flags = 0, .data = "\x01\x01\x02\x20" },
2459         .{ .kind = @backingInt(SectionKind.attributes), .flags = 0, .data = "\x01\x03\x01" },
2460         .{ .kind = @backingInt(SectionKind.locations), .flags = 0, .data = "\x02\x00\x03\x01\x00" },
2461         .{ .kind = @backingInt(SectionKind.module), .flags = 0, .data = "" },
2462     };
2463     if (replacement) |value| {
2464         for (&sections) |*section| {
2465             if (section.kind == value.kind) section.* = value;
2466         }
2467     }
2468     return writeContainer(allocator, &sections);
2469 }
2470 
2471 test "bytecode table decoders release partial malformed containers" {
2472     const allocator = std.testing.allocator;
2473     const cases = [_]struct { kind: SectionKind, data: []const u8, err: anyerror }{
2474         .{ .kind = .strings, .data = "\x02\x01a", .err = error.EndOfStream },
2475         .{ .kind = .dialects, .data = "\x02\x00\x00\x00", .err = error.EndOfStream },
2476         .{ .kind = .types, .data = "\x02\x01\x02\x20", .err = error.EndOfStream },
2477         .{ .kind = .attributes, .data = "\x02\x03\x01", .err = error.EndOfStream },
2478         .{ .kind = .locations, .data = "\x02\x00", .err = error.EndOfStream },
2479         .{ .kind = .locations, .data = "\x02\x00\x03\x02\x00", .err = error.EndOfStream },
2480         .{ .kind = .locations, .data = "\x03\x00\x03\x01\x00", .err = error.EndOfStream },
2481         .{ .kind = .dialects, .data = "\x01\x02\x00\x00", .err = error.InvalidTable },
2482         .{ .kind = .locations, .data = "\x02\x00\x03\x01\x02", .err = error.InvalidTable },
2483     };
2484     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2485     defer ctx.deinit(allocator);
2486     try ctx.allowUnregistered();
2487     for (cases) |case| {
2488         const bytes = try testTableContainer(allocator, .{
2489             .kind = @backingInt(case.kind),
2490             .flags = 0,
2491             .data = case.data,
2492         });
2493         defer allocator.free(bytes);
2494         try std.testing.expectError(case.err, decodeTables(allocator, &ctx, bytes));
2495         try std.testing.expectError(case.err, decodeModule(allocator, &ctx, bytes));
2496     }
2497 }
2498 
2499 test "bytecode table decoders reject every invalid scalar tag" {
2500     const allocator = std.testing.allocator;
2501     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2502     defer ctx.deinit(allocator);
2503     for (4..256) |tag| {
2504         const types = [_]u8{ 1, @backingInt(TypeKind.builtin_scalar), @intCast(tag), 32 };
2505         const bytes = try testTableContainer(allocator, .{
2506             .kind = @backingInt(SectionKind.types),
2507             .flags = 0,
2508             .data = &types,
2509         });
2510         defer allocator.free(bytes);
2511         try std.testing.expectError(error.UnsupportedType, decodeTables(allocator, &ctx, bytes));
2512         try std.testing.expectError(error.UnsupportedType, decodeModule(allocator, &ctx, bytes));
2513     }
2514 }
2515 
2516 test "bytecode table decoders release every failed allocation" {
2517     const allocator = std.testing.allocator;
2518     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2519     defer ctx.deinit(allocator);
2520     try ctx.allowUnregistered();
2521     const bytes = try testTableContainer(allocator, null);
2522     defer allocator.free(bytes);
2523     try std.testing.checkAllAllocationFailures(allocator, testTableAllocationFailure, .{ &ctx, bytes });
2524 }
2525 
2526 fn testTableAllocationFailure(
2527     allocator: std.mem.Allocator,
2528     ctx: *ir.Context,
2529     bytes: []const u8,
2530 ) !void {
2531     var tables = try decodeTables(allocator, ctx, bytes);
2532     defer tables.deinit();
2533     try std.testing.expectEqual(@as(usize, 2), tables.strings.len);
2534     try std.testing.expectEqual(@as(usize, 1), tables.dialects.len);
2535     try std.testing.expectEqual(@as(usize, 1), tables.types.len);
2536     try std.testing.expectEqual(@as(usize, 1), tables.attrs.len);
2537     try std.testing.expectEqual(@as(usize, 2), tables.locs.len);
2538     try std.testing.expectEqual(@as(usize, 1), tables.locs[1].fused.locations.len);
2539     try std.testing.expectEqual(@as(usize, 1), tables.loc_slices.items.len);
2540 }
2541 
2542 test "bytecode encoding is deterministic" {
2543     const testing = std.testing;
2544     const allocator = testing.allocator;
2545     const test_dialect = @import("../dialects/fixture/root.zig");
2546 
2547     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2548     defer ctx.deinit(allocator);
2549     try ctx.allowUnregistered();
2550 
2551     var builder = TableBuilder.init(allocator);
2552     defer builder.deinit();
2553 
2554     const i32_type = try test_dialect.TestDialect.getI32Type(&ctx);
2555     const attr = try test_dialect.TestDialect.getIntegerAttr(&ctx, 7);
2556     _ = try builder.internType(i32_type);
2557     _ = try builder.internAttribute(attr);
2558     _ = try builder.internLocation(ir.Location.getUnknown());
2559 
2560     const bytes1 = try encodeTables(allocator, &builder);
2561     defer allocator.free(bytes1);
2562     const bytes2 = try encodeTables(allocator, &builder);
2563     defer allocator.free(bytes2);
2564 
2565     try testing.expectEqualSlices(u8, bytes1, bytes2);
2566 }
2567 
2568 test "bytecode rejects invalid magic" {
2569     const testing = std.testing;
2570     const allocator = testing.allocator;
2571 
2572     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2573     defer ctx.deinit(allocator);
2574 
2575     var builder = TableBuilder.init(allocator);
2576     defer builder.deinit();
2577 
2578     const bytes = try encodeTables(allocator, &builder);
2579     defer allocator.free(bytes);
2580 
2581     var corrupted = try allocator.dupe(u8, bytes);
2582     defer allocator.free(corrupted);
2583     corrupted[0] = 'X';
2584 
2585     try testing.expectError(error.InvalidHeader, decodeTables(allocator, &ctx, corrupted));
2586 }
2587 
2588 test "bytecode rejects overflowed section ranges" {
2589     const testing = std.testing;
2590     const allocator = testing.allocator;
2591 
2592     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2593     defer ctx.deinit(allocator);
2594 
2595     var builder = TableBuilder.init(allocator);
2596     defer builder.deinit();
2597 
2598     const bytes = try encodeTables(allocator, &builder);
2599     defer allocator.free(bytes);
2600 
2601     var section_count = try allocator.dupe(u8, bytes);
2602     defer allocator.free(section_count);
2603     const section_count_offset = magic.len + 4 + 1 + 1 + 2 + 8;
2604     std.mem.writeInt(
2605         u64,
2606         section_count[section_count_offset..][0..8],
2607         std.math.maxInt(u64),
2608         .little,
2609     );
2610     try testing.expectError(
2611         error.InvalidSectionTable,
2612         decodeTables(allocator, &ctx, section_count),
2613     );
2614 
2615     var table_offset = try allocator.dupe(u8, bytes);
2616     defer allocator.free(table_offset);
2617     const table_offset_offset = magic.len + 4 + 1 + 1 + 2;
2618     std.mem.writeInt(
2619         u64,
2620         table_offset[table_offset_offset..][0..8],
2621         std.math.maxInt(u64),
2622         .little,
2623     );
2624     try testing.expectError(
2625         error.InvalidSectionTable,
2626         decodeTables(allocator, &ctx, table_offset),
2627     );
2628 
2629     var section_range = try allocator.dupe(u8, bytes);
2630     defer allocator.free(section_range);
2631     const first_section_offset: usize = @intCast(headerSize() + 8);
2632     std.mem.writeInt(
2633         u64,
2634         section_range[first_section_offset..][0..8],
2635         std.math.maxInt(u64),
2636         .little,
2637     );
2638     std.mem.writeInt(
2639         u64,
2640         section_range[first_section_offset + 8 ..][0..8],
2641         1,
2642         .little,
2643     );
2644     try testing.expectError(
2645         error.InvalidSection,
2646         decodeTables(allocator, &ctx, section_range),
2647     );
2648 }
2649 
2650 test "bytecode reader rejects overflowing lengths" {
2651     var empty = SliceReader.init("");
2652     try std.testing.expectError(
2653         error.EndOfStream,
2654         empty.readBytes(std.math.maxInt(usize)),
2655     );
2656 
2657     const continued = [_]u8{
2658         0x80,
2659         0x80,
2660         0x80,
2661         0x80,
2662         0x80,
2663         0x80,
2664         0x80,
2665         0x80,
2666         0x80,
2667         0x80,
2668     };
2669     var unsigned_reader = SliceReader.init(&continued);
2670     try std.testing.expectError(
2671         error.InvalidLEB128,
2672         unsigned_reader.readULEB(),
2673     );
2674     var signed_reader = SliceReader.init(&continued);
2675     try std.testing.expectError(
2676         error.InvalidLEB128,
2677         signed_reader.readSLEB(),
2678     );
2679 }
2680 
2681 test "bytecode exposes format version policy" {
2682     const current = classifyFormatVersion(format_version);
2683     try std.testing.expect(current.readable());
2684     try std.testing.expectEqual(FormatVersionStatus.readable, current.status);
2685     try std.testing.expectEqual(format_version, current.version);
2686     try std.testing.expectEqual(minimum_readable_version, current.minimum_readable);
2687     try std.testing.expectEqual(maximum_readable_version, current.maximum_readable);
2688 
2689     const older = classifyFormatVersion(minimum_readable_version - 1);
2690     try std.testing.expect(!older.readable());
2691     try std.testing.expectEqual(FormatVersionStatus.unsupported_older, older.status);
2692 
2693     const newer = classifyFormatVersion(maximum_readable_version + 1);
2694     try std.testing.expect(!newer.readable());
2695     try std.testing.expectEqual(FormatVersionStatus.unsupported_newer, newer.status);
2696 }
2697 
2698 test "bytecode inspects and rejects unsupported versions" {
2699     const testing = std.testing;
2700     const allocator = testing.allocator;
2701 
2702     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2703     defer ctx.deinit(allocator);
2704 
2705     var builder = TableBuilder.init(allocator);
2706     defer builder.deinit();
2707 
2708     const bytes = try encodeTables(allocator, &builder);
2709     defer allocator.free(bytes);
2710 
2711     const header = try inspectHeader(bytes);
2712     try testing.expectEqual(format_version, header.version);
2713     try testing.expect(header.compatibility().readable());
2714 
2715     const corrupted_future = try allocator.dupe(u8, bytes);
2716     defer allocator.free(corrupted_future);
2717     writeTestVersion(corrupted_future, maximum_readable_version + 1);
2718     const future_header = try inspectHeader(corrupted_future);
2719     try testing.expectEqual(FormatVersionStatus.unsupported_newer, future_header.compatibility().status);
2720     try testing.expectError(error.UnsupportedVersion, decodeTables(allocator, &ctx, corrupted_future));
2721 
2722     const corrupted_previous = try allocator.dupe(u8, bytes);
2723     defer allocator.free(corrupted_previous);
2724     writeTestVersion(corrupted_previous, minimum_readable_version - 1);
2725     const previous_header = try inspectHeader(corrupted_previous);
2726     try testing.expectEqual(FormatVersionStatus.unsupported_older, previous_header.compatibility().status);
2727     try testing.expectError(error.UnsupportedVersion, decodeTables(allocator, &ctx, corrupted_previous));
2728 
2729     const corrupted_old = try allocator.dupe(u8, bytes);
2730     defer allocator.free(corrupted_old);
2731     writeTestVersion(corrupted_old, 3);
2732     try testing.expectError(error.UnsupportedVersion, decodeTables(allocator, &ctx, corrupted_old));
2733 }
2734 
2735 test "bytecode ignores unknown sections" {
2736     const testing = std.testing;
2737     const allocator = testing.allocator;
2738 
2739     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2740     defer ctx.deinit(allocator);
2741 
2742     var builder = TableBuilder.init(allocator);
2743     defer builder.deinit();
2744     _ = try builder.internLocation(ir.Location.getUnknown());
2745 
2746     const strings_data = try encodeStringsSection(allocator, &builder);
2747     defer allocator.free(strings_data);
2748     const dialects_data = try encodeDialectsSection(allocator, &builder);
2749     defer allocator.free(dialects_data);
2750     const types_data = try encodeTypesSection(allocator, &builder);
2751     defer allocator.free(types_data);
2752     const attrs_data = try encodeAttributesSection(allocator, &builder);
2753     defer allocator.free(attrs_data);
2754     const locs_data = try encodeLocationsSection(allocator, &builder);
2755     defer allocator.free(locs_data);
2756 
2757     const unknown_payload = try allocator.dupe(u8, "junk");
2758     defer allocator.free(unknown_payload);
2759 
2760     const sections = [_]SectionData{
2761         .{ .kind = @backingInt(SectionKind.strings), .flags = 0, .data = strings_data },
2762         .{ .kind = @backingInt(SectionKind.dialects), .flags = 0, .data = dialects_data },
2763         .{ .kind = @backingInt(SectionKind.types), .flags = 0, .data = types_data },
2764         .{ .kind = @backingInt(SectionKind.attributes), .flags = 0, .data = attrs_data },
2765         .{ .kind = @backingInt(SectionKind.locations), .flags = 0, .data = locs_data },
2766         .{ .kind = 99, .flags = 0, .data = unknown_payload },
2767     };
2768 
2769     const bytes = try writeContainer(allocator, &sections);
2770     defer allocator.free(bytes);
2771 
2772     var tables = try decodeTables(allocator, &ctx, bytes);
2773     defer tables.deinit();
2774 
2775     try testing.expectEqual(@as(usize, 1), tables.locs.len);
2776 }
2777 
2778 test "bytecode module round-trip" {
2779     const testing = std.testing;
2780     const allocator = testing.allocator;
2781     const test_dialect = @import("../dialects/fixture/root.zig");
2782 
2783     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2784     defer ctx.deinit(allocator);
2785     try ctx.allowUnregistered();
2786 
2787     const loc = ir.Location.getUnknown();
2788     const argument_loc = ir.Location.getFileRange(
2789         "roundtrip.mlir",
2790         .{ .byte = 4, .line = 2, .column = 3 },
2791         .{ .byte = 9, .line = 2, .column = 8 },
2792     );
2793     const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
2794     const module_region = module_op.getBody();
2795     const entry_block = module_op.getBodyBlock();
2796 
2797     const i32_type = try test_dialect.TestDialect.getI32Type(&ctx);
2798     _ = try entry_block.addArgument(i32_type, argument_loc);
2799     _ = try entry_block.addArgument(i32_type, loc);
2800     const arg0 = entry_block.getArgument(0).?;
2801     const arg1 = entry_block.getArgument(1).?;
2802 
2803     const answer_attr = try test_dialect.TestDialect.getIntegerAttr(&ctx, 42);
2804     const add_attrs = [_]ir.NamedAttribute{.{ .name = "answer", .value = answer_attr }};
2805 
2806     var add_state = ir.Operation.State.init("test.add", loc);
2807     add_state.addOperands(&.{ arg0, arg1 });
2808     add_state.addTypes(&.{i32_type});
2809     add_state.addAttributes(&add_attrs);
2810     const add_op = try ctx.createOperation(add_state);
2811     try entry_block.addOperation(add_op);
2812 
2813     var use_state = ir.Operation.State.init("test.use", loc);
2814     use_state.addOperands(&.{add_op.getResult(0).?});
2815     const use_op = try ctx.createOperation(use_state);
2816     try entry_block.addOperation(use_op);
2817 
2818     const second_block = try module_region.addBlock();
2819 
2820     var br_state = ir.Operation.State.init("test.br", loc);
2821     br_state.addSuccessors(&.{second_block});
2822     const br_op = try ctx.createOperation(br_state);
2823     try entry_block.addOperation(br_op);
2824 
2825     var nested_state = ir.Operation.State.init("test.with_region", loc);
2826     nested_state.addRegion();
2827     const nested_op = try ctx.createOperation(nested_state);
2828     try entry_block.addOperation(nested_op);
2829 
2830     const nested_region = nested_op.getRegion(0).?;
2831     const nested_block = try nested_region.addBlock();
2832     const inner_state = ir.Operation.State.init("test.inner", loc);
2833     const inner_op = try ctx.createOperation(inner_state);
2834     try nested_block.addOperation(inner_op);
2835 
2836     const bytes = try encodeModule(allocator, module_op.op);
2837     defer allocator.free(bytes);
2838 
2839     var decode_ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2840     defer decode_ctx.deinit(allocator);
2841     try decode_ctx.allowUnregistered();
2842 
2843     var decoded = try decodeModule(allocator, &decode_ctx, bytes);
2844     defer decoded.deinit();
2845 
2846     try testing.expect(decoded.format().readable());
2847     try testing.expectEqual(format_version, decoded.format().version);
2848     const decoded_module = decoded.module;
2849     try testing.expectEqualStrings("test.module", decoded_module.name.name);
2850     try testing.expectEqual(@as(usize, 0), decoded.resources.len);
2851 
2852     const decoded_region = decoded_module.getRegion(0).?;
2853     var block_iter = decoded_region.getBlocks();
2854     const decoded_entry = block_iter.next() orelse return error.TestExpectedBlock;
2855     const decoded_second = block_iter.next() orelse return error.TestExpectedBlock;
2856     try testing.expect(block_iter.next() == null);
2857 
2858     try testing.expectEqual(@as(usize, 2), decoded_entry.getNumArguments());
2859     const decoded_i32 = try test_dialect.TestDialect.getI32Type(&decode_ctx);
2860     try testing.expect(decoded_entry.getArgument(0).?.type.eql(decoded_i32));
2861     try testing.expect(decoded_entry.getArgument(1).?.type.eql(decoded_i32));
2862     try testing.expect(decoded_entry.getArgumentLocation(0).?.eql(argument_loc));
2863     try testing.expect(decoded_entry.getArgumentLocation(1).?.eql(loc));
2864 
2865     const head_any = decoded_entry.operations.head orelse return error.TestExpectedOp;
2866     const op_add: *ir.Operation = @ptrCast(@alignCast(head_any));
2867     try testing.expectEqualStrings("test.add", op_add.name.name);
2868 
2869     const decoded_answer = op_add.getAttr("answer") orelse return error.TestExpectedAttr;
2870     const decoded_value = test_dialect.TestDialect.getIntegerValue(decoded_answer) orelse return error.TestExpectedAttr;
2871     try testing.expectEqual(@as(i64, 42), decoded_value);
2872 
2873     const op_use = op_add.next_op orelse return error.TestExpectedOp;
2874     try testing.expectEqualStrings("test.use", op_use.name.name);
2875     try testing.expect(op_use.getOperand(0).? == op_add.getResult(0).?);
2876 
2877     const op_br = op_use.next_op orelse return error.TestExpectedOp;
2878     try testing.expectEqualStrings("test.br", op_br.name.name);
2879     try testing.expectEqual(@as(usize, 1), op_br.getNumSuccessors());
2880     try testing.expect(op_br.getSuccessor(0).? == decoded_second);
2881 
2882     const op_nested = op_br.next_op orelse return error.TestExpectedOp;
2883     try testing.expectEqualStrings("test.with_region", op_nested.name.name);
2884     try testing.expectEqual(@as(usize, 1), op_nested.getNumRegions());
2885 
2886     const nested_region_dec = op_nested.getRegion(0).?;
2887     const nested_entry = nested_region_dec.getEntryBlock() orelse return error.TestExpectedBlock;
2888     const nested_head = nested_entry.operations.head orelse return error.TestExpectedOp;
2889     const decoded_inner: *ir.Operation = @ptrCast(@alignCast(nested_head));
2890     try testing.expectEqualStrings("test.inner", decoded_inner.name.name);
2891 }
2892 
2893 test "bytecode flat module decoder retains one top-level operation" {
2894     const testing = std.testing;
2895     const allocator = testing.allocator;
2896     const dialects = @import("../dialects/root.zig");
2897 
2898     var encode_ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2899     defer encode_ctx.deinit(allocator);
2900     try dialects.registerChoirDialect(&encode_ctx);
2901     try encode_ctx.allowUnregistered();
2902 
2903     const loc = ir.Location.getUnknown();
2904     const module = try dialects.BuiltinDialect.ModuleOp.create(
2905         &encode_ctx,
2906         loc,
2907     );
2908     const int = try dialects.ArithDialect.getScalarType(&encode_ctx, .i32);
2909     var first_state = ir.Operation.State.init("test.first", loc);
2910     first_state.addTypes(&.{int});
2911     try module.getBodyBlock().addOperation(
2912         try encode_ctx.createOperation(first_state),
2913     );
2914     var second_state = ir.Operation.State.init("test.second", loc);
2915     second_state.addTypes(&.{int});
2916     try module.getBodyBlock().addOperation(
2917         try encode_ctx.createOperation(second_state),
2918     );
2919 
2920     const bytes = try encodeModule(allocator, module.op);
2921     defer allocator.free(bytes);
2922 
2923     var decode_ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2924     defer decode_ctx.deinit(allocator);
2925     try dialects.registerChoirDialect(&decode_ctx);
2926     try decode_ctx.allowUnregistered();
2927 
2928     var decoded = try FlatModuleDecoder.init(
2929         allocator,
2930         &decode_ctx,
2931         bytes,
2932     );
2933     defer decoded.deinit();
2934     try testing.expectEqualStrings(
2935         dialects.BuiltinDialect.ModuleOp.operation_name,
2936         decoded.name(),
2937     );
2938     try testing.expectEqualStrings("test.first", (try decoded.next()).?.name.name);
2939     try testing.expectEqual(@as(usize, 1), decode_ctx.operationCount());
2940     try testing.expectEqualStrings("test.second", (try decoded.next()).?.name.name);
2941     try testing.expectEqual(@as(usize, 1), decode_ctx.operationCount());
2942     try testing.expect((try decoded.next()) == null);
2943     try testing.expectEqual(@as(usize, 0), decode_ctx.operationCount());
2944 }
2945 
2946 test "bytecode flat module decoder rejects cross-operation values" {
2947     const testing = std.testing;
2948     const allocator = testing.allocator;
2949     const dialects = @import("../dialects/root.zig");
2950 
2951     var encode_ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2952     defer encode_ctx.deinit(allocator);
2953     try dialects.registerChoirDialect(&encode_ctx);
2954     try encode_ctx.allowUnregistered();
2955 
2956     const loc = ir.Location.getUnknown();
2957     const module = try dialects.BuiltinDialect.ModuleOp.create(
2958         &encode_ctx,
2959         loc,
2960     );
2961     const int = try dialects.ArithDialect.getScalarType(&encode_ctx, .i32);
2962     var producer_state = ir.Operation.State.init("test.producer", loc);
2963     producer_state.addTypes(&.{int});
2964     const producer = try encode_ctx.createOperation(producer_state);
2965     try module.getBodyBlock().addOperation(producer);
2966     var consumer_state = ir.Operation.State.init("test.consumer", loc);
2967     consumer_state.addOperands(&.{producer.getResult(0).?});
2968     try module.getBodyBlock().addOperation(
2969         try encode_ctx.createOperation(consumer_state),
2970     );
2971 
2972     const bytes = try encodeModule(allocator, module.op);
2973     defer allocator.free(bytes);
2974 
2975     var decode_ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
2976     defer decode_ctx.deinit(allocator);
2977     try dialects.registerChoirDialect(&decode_ctx);
2978     try decode_ctx.allowUnregistered();
2979 
2980     var decoded = try FlatModuleDecoder.init(
2981         allocator,
2982         &decode_ctx,
2983         bytes,
2984     );
2985     defer decoded.deinit();
2986     try testing.expectEqualStrings(
2987         "test.producer",
2988         (try decoded.next()).?.name.name,
2989     );
2990     try testing.expectError(error.InvalidModule, decoded.next());
2991 }
2992 
2993 test "bytecode round-trips read-only properties and raw shadows" {
2994     const testing = std.testing;
2995     const allocator = testing.allocator;
2996 
2997     var encode_context = try ir.Context.init(allocator, ir.Context.Limits.testing);
2998     defer encode_context.deinit(allocator);
2999     try registerBytecodeProperties(&encode_context);
3000 
3001     const left = try encode_context.getI64Attr(11);
3002     const right = try encode_context.getI64Attr(22);
3003     const payload = try encode_context.getArrayAttr(&.{ left, right });
3004     const raw_left = try encode_context.getI64Attr(99);
3005     const note = try encode_context.getStringAttr("kept");
3006     const raw_attributes = [_]ir.NamedAttribute{
3007         .{ .name = "left", .value = raw_left },
3008         .{ .name = "debug.note", .value = note },
3009     };
3010     var state = ir.Operation.State.init("test.bytecode_properties", ir.Location.getUnknown());
3011     state.addRawAttributes(&raw_attributes);
3012     try state.setPropertiesAttr(payload);
3013     const source = try encode_context.createOperation(state);
3014 
3015     const encoded = try encodeModule(allocator, source);
3016     defer allocator.free(encoded);
3017 
3018     var decode_context = try ir.Context.init(allocator, ir.Context.Limits.testing);
3019     defer decode_context.deinit(allocator);
3020     try registerBytecodeProperties(&decode_context);
3021     var decoded = try decodeModule(allocator, &decode_context, encoded);
3022     defer decoded.deinit();
3023 
3024     const decoded_left = decoded.module.getAttr("left") orelse return error.TestExpectedAttr;
3025     const decoded_right = decoded.module.getAttr("right") orelse return error.TestExpectedAttr;
3026     try testing.expectEqual(@as(i64, 11), decoded_left.cast(ir.Attribute.IntegerAttr).?.value);
3027     try testing.expectEqual(@as(i64, 22), decoded_right.cast(ir.Attribute.IntegerAttr).?.value);
3028     try testing.expectEqual(@as(usize, 2), decoded.module.getRawDictionaryAttrs().len);
3029     try testing.expectEqual(
3030         @as(i64, 99),
3031         decoded.module.raw_dictionary_attrs.get("left").?.cast(ir.Attribute.IntegerAttr).?.value,
3032     );
3033     try testing.expectEqualStrings(
3034         "kept",
3035         decoded.module.getAttrAs(ir.Attribute.StringAttr, "debug.note").?.value,
3036     );
3037 
3038     const reencoded = try encodeModule(allocator, decoded.module);
3039     defer allocator.free(reencoded);
3040     try testing.expectEqualSlices(u8, encoded, reencoded);
3041 }
3042 
3043 test "bytecode module round-trips resources" {
3044     const testing = std.testing;
3045     const allocator = testing.allocator;
3046     const test_dialect = @import("../dialects/fixture/root.zig");
3047 
3048     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
3049     defer ctx.deinit(allocator);
3050 
3051     const loc = ir.Location.getUnknown();
3052     const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
3053 
3054     const resources = [_]Resource{
3055         .{
3056             .namespace = "choir.artifact",
3057             .name = "manifest",
3058             .type_id = "choir.backends.artifact.Manifest/v1",
3059             .data = "{\"target\":\"x86_64\"}",
3060         },
3061         .{
3062             .namespace = "accy.artifact",
3063             .name = "kernel-metadata",
3064             .type_id = "accy.cuda.KernelMetadata/v1",
3065             .data = "entry=add_f32",
3066         },
3067     };
3068 
3069     const bytes = try encodeModuleWithResources(allocator, module_op.op, &resources);
3070     defer allocator.free(bytes);
3071 
3072     var decode_ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
3073     defer decode_ctx.deinit(allocator);
3074     try test_dialect.registerTestDialect(&decode_ctx);
3075 
3076     var decoded = try decodeModule(allocator, &decode_ctx, bytes);
3077     defer decoded.deinit();
3078 
3079     try testing.expectEqualStrings("test.module", decoded.module.name.name);
3080     try testing.expectEqual(resources.len, decoded.resources.len);
3081     for (resources, decoded.resources) |expected, actual| {
3082         try testing.expectEqualStrings(expected.namespace, actual.namespace);
3083         try testing.expectEqualStrings(expected.name, actual.name);
3084         try testing.expectEqualStrings(expected.type_id, actual.type_id);
3085         try testing.expectEqualStrings(expected.data, actual.data);
3086     }
3087 }
3088 
3089 test "bytecode round-trips undotted dialect type names" {
3090     const testing = std.testing;
3091     const allocator = testing.allocator;
3092     const arith = @import("../dialects/root.zig").ArithDialect;
3093     const root_dialects = @import("../dialects/root.zig");
3094     const MemrefDialect = root_dialects.MemrefDialect;
3095     const interfaces = ir.interfaces;
3096 
3097     var enc_ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
3098     defer enc_ctx.deinit(allocator);
3099     try root_dialects.registerChoirDialect(&enc_ctx);
3100 
3101     const f32_type = try arith.getScalarType(&enc_ctx, .f32);
3102     const memref_type = try MemrefDialect.getMemrefType1D(&enc_ctx, 1024, f32_type, .host);
3103 
3104     var builder = TableBuilder.init(allocator);
3105     defer builder.deinit();
3106     _ = try builder.internType(memref_type);
3107 
3108     const bytes = try encodeTables(allocator, &builder);
3109     defer allocator.free(bytes);
3110 
3111     var dec_ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
3112     defer dec_ctx.deinit(allocator);
3113     try dec_ctx.requireRegistered();
3114     try root_dialects.registerChoirDialect(&dec_ctx);
3115 
3116     var tables = try decodeTables(allocator, &dec_ctx, bytes);
3117     defer tables.deinit();
3118 
3119     try testing.expectEqual(@as(usize, 1), tables.types.len);
3120     const decoded = tables.types[0];
3121 
3122     const storage = decoded.getDialectStorage() orelse return error.TestExpectedDialectStorage;
3123     try testing.expectEqualStrings("memref", storage.name);
3124     try testing.expectEqualStrings("1024,arith.f32,host", storage.param_key);
3125 
3126     try testing.expect((try dec_ctx.getTypeParamPayload(decoded, MemrefDialect.MemrefTypePayload)) != null);
3127     try testing.expect(dec_ctx.typeInterface(decoded, interfaces.ShapedTypeInterface) != null);
3128 
3129     try testing.expect(hasDialect(tables.dialects, "memref"));
3130 }
3131 
3132 fn hasType(types: []const ir.Type, needle: ir.Type) bool {
3133     for (types) |item| {
3134         if (item.eql(needle)) return true;
3135     }
3136     return false;
3137 }
3138 
3139 fn writeTestVersion(bytes: []u8, version: u32) void {
3140     std.mem.writeInt(u32, bytes[magic.len..][0..4], version, .little);
3141 }
3142 
3143 fn hasDialect(dialects: []const DialectEntry, needle: []const u8) bool {
3144     for (dialects) |item| {
3145         if (std.mem.eql(u8, item.name, needle)) return true;
3146     }
3147     return false;
3148 }
3149 
3150 fn hasAttr(attrs: []const ir.Attribute, needle: ir.Attribute) bool {
3151     for (attrs) |item| {
3152         if (attrEqual(item, needle)) return true;
3153     }
3154     return false;
3155 }
3156 
3157 fn hasLocation(locs: []const ir.Location, needle: ir.Location) bool {
3158     for (locs) |item| {
3159         if (locationEqual(item, needle)) return true;
3160     }
3161     return false;
3162 }