lib/choir/src/core/symbols.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const alloc_arena = @import("alloc_arena");
   3 const attribute = @import("attribute.zig");
   4 const Attribute = attribute.Attribute;
   5 const NamedAttribute = attribute.NamedAttribute;
   6 const Operation = @import("operation/root.zig").Operation;
   7 const Region = @import("region.zig").Region;
   8 const interfaces = @import("interfaces/root.zig");
   9 
  10 const AttributeReplacement = struct {
  11     attr: ?Attribute = null,
  12     replaced: usize = 0,
  13 };
  14 
  15 const SymbolTableSemanticError = error{
  16     DuplicateSymbol,
  17     InvalidSymbol,
  18     InvalidSymbolTable,
  19     InvalidSymbolDeclaration,
  20 };
  21 
  22 pub const SymbolTableError = SymbolTableSemanticError || std.mem.Allocator.Error;
  23 
  24 pub const SymbolUse = struct {
  25     user: *Operation,
  26     attr_name: []const u8,
  27     symbol_ref: *const Attribute.SymbolRefAttr,
  28 };
  29 
  30 pub const SymbolUseRange = struct {
  31     uses: std.ArrayList(SymbolUse) = .empty,
  32 
  33     pub fn deinit(self: *SymbolUseRange, allocator: std.mem.Allocator) void {
  34         self.uses.deinit(allocator);
  35         self.* = .{};
  36     }
  37 
  38     pub fn items(self: *const SymbolUseRange) []const SymbolUse {
  39         return self.uses.items;
  40     }
  41 
  42     pub fn empty(self: *const SymbolUseRange) bool {
  43         return self.uses.items.len == 0;
  44     }
  45 
  46     fn append(self: *SymbolUseRange, allocator: std.mem.Allocator, use: SymbolUse) !void {
  47         try self.uses.append(allocator, use);
  48     }
  49 };
  50 
  51 pub const SymbolTable = struct {
  52     allocator: std.mem.Allocator,
  53     symbols: std.StringHashMapUnmanaged(*Operation),
  54 
  55     pub const symbol_attr_names = struct {
  56         pub const sym_name = "sym_name";
  57         pub const sym_visibility = "sym_visibility";
  58     };
  59 
  60     pub const Visibility = enum {
  61         public,
  62         private,
  63         nested,
  64 
  65         pub fn fromString(value: []const u8) ?Visibility {
  66             if (std.mem.eql(u8, value, "public")) return .public;
  67             if (std.mem.eql(u8, value, "private")) return .private;
  68             if (std.mem.eql(u8, value, "nested")) return .nested;
  69             return null;
  70         }
  71 
  72         pub fn toString(self: Visibility) []const u8 {
  73             return @tagName(self);
  74         }
  75     };
  76 
  77     pub const Collection = struct {
  78         allocator: std.mem.Allocator,
  79         tables: std.AutoArrayHashMapUnmanaged(*Operation, SymbolTable) = .empty,
  80 
  81         pub fn init(allocator: std.mem.Allocator) Collection {
  82             return .{ .allocator = allocator };
  83         }
  84 
  85         pub fn deinit(self: *Collection) void {
  86             for (self.tables.values()) |*table| {
  87                 table.deinit();
  88             }
  89             self.tables.deinit(self.allocator);
  90             self.tables = .empty;
  91         }
  92 
  93         pub fn getSymbolTable(self: *Collection, op: *Operation) SymbolTableError!*SymbolTable {
  94             if (!isSymbolTableOperation(op)) return error.InvalidSymbolTable;
  95 
  96             const entry = try self.tables.getOrPut(self.allocator, op);
  97             if (!entry.found_existing) {
  98                 entry.value_ptr.* = SymbolTable.init(self.allocator);
  99                 entry.value_ptr.buildFromOperation(op) catch |err| {
 100                     entry.value_ptr.deinit();
 101                     _ = self.tables.swapRemove(op);
 102                     return err;
 103                 };
 104             }
 105             return entry.value_ptr;
 106         }
 107 
 108         pub fn invalidateSymbolTable(self: *Collection, op: *Operation) void {
 109             if (self.tables.getPtr(op)) |table| {
 110                 table.deinit();
 111                 _ = self.tables.swapRemove(op);
 112             }
 113         }
 114 
 115         pub fn lookupSymbolIn(
 116             self: *Collection,
 117             op: *Operation,
 118             name: []const u8,
 119         ) SymbolTableError!?*Operation {
 120             if (!isSymbolTableOperation(op)) return null;
 121             const table = try self.getSymbolTable(op);
 122             return table.lookup(name);
 123         }
 124 
 125         pub fn lookupSymbolRefIn(
 126             self: *Collection,
 127             op: *Operation,
 128             symbol_ref: *const Attribute.SymbolRefAttr,
 129         ) SymbolTableError!?*Operation {
 130             var resolved = try self.lookupSymbolIn(op, symbol_ref.getRootReference()) orelse return null;
 131             for (symbol_ref.getNestedReferences()) |nested_ref| {
 132                 resolved = try self.lookupSymbolIn(resolved, nested_ref) orelse return null;
 133                 if (getSymbolVisibility(resolved) == .private) return null;
 134             }
 135             return resolved;
 136         }
 137 
 138         pub fn lookupNearestSymbolFrom(
 139             self: *Collection,
 140             from: *Operation,
 141             name: []const u8,
 142         ) SymbolTableError!?*Operation {
 143             const table_op = getNearestSymbolTable(from) orelse return null;
 144             return self.lookupSymbolIn(table_op, name);
 145         }
 146 
 147         pub fn lookupNearestSymbolRefFrom(
 148             self: *Collection,
 149             from: *Operation,
 150             symbol_ref: *const Attribute.SymbolRefAttr,
 151         ) SymbolTableError!?*Operation {
 152             const table_op = getNearestSymbolTable(from) orelse return null;
 153             return self.lookupSymbolRefIn(table_op, symbol_ref);
 154         }
 155     };
 156 
 157     pub fn init(allocator: std.mem.Allocator) SymbolTable {
 158         return .{
 159             .allocator = allocator,
 160             .symbols = .{},
 161         };
 162     }
 163 
 164     pub fn deinit(self: *SymbolTable) void {
 165         self.symbols.deinit(self.allocator);
 166     }
 167 
 168     pub fn clearRetainingCapacity(self: *SymbolTable) void {
 169         self.symbols.clearRetainingCapacity();
 170     }
 171 
 172     pub fn lookup(self: *const SymbolTable, name: []const u8) ?*Operation {
 173         return self.symbols.get(name);
 174     }
 175 
 176     pub fn iterator(self: *SymbolTable) std.StringHashMapUnmanaged(*Operation).Iterator {
 177         return self.symbols.iterator();
 178     }
 179 
 180     pub fn buildFromOperation(self: *SymbolTable, op: *Operation) SymbolTableError!void {
 181         self.clearRetainingCapacity();
 182         if (!isSymbolTableOperation(op)) return error.InvalidSymbolTable;
 183         if (op.regions.items.len != 1) return error.InvalidSymbolTable;
 184         const region = &op.regions.items[0];
 185         if (region.blocks.size != 1) return error.InvalidSymbolTable;
 186         try self.appendFromRegion(region);
 187     }
 188 
 189     fn appendFromRegion(self: *SymbolTable, region: *Region) SymbolTableError!void {
 190         const block = region.blocks.front() orelse return;
 191         var ops = block.getOperations();
 192         while (ops.next()) |op| {
 193             if (getSymbolName(op)) |name| {
 194                 try self.addSymbol(name, op);
 195             }
 196         }
 197     }
 198 
 199     fn addSymbol(self: *SymbolTable, name: []const u8, op: *Operation) SymbolTableError!void {
 200         const entry = try self.symbols.getOrPut(self.allocator, name);
 201         if (entry.found_existing) return error.DuplicateSymbol;
 202         entry.value_ptr.* = op;
 203     }
 204 
 205     pub fn isSymbolTableOperation(op: *Operation) bool {
 206         return op.getTraits().is_symbol_table;
 207     }
 208 
 209     pub fn isSymbolOperation(op: *Operation) bool {
 210         return op.interface(interfaces.SymbolOpInterface) != null;
 211     }
 212 
 213     pub fn verifyOperation(op: *Operation) anyerror!void {
 214         var symbol_tables = Collection.init(op.allocator);
 215         defer symbol_tables.deinit();
 216         _ = try symbol_tables.getSymbolTable(op);
 217         try verifySymbols(op);
 218         try verifySymbolUsesInSymbolTable(&symbol_tables, op);
 219     }
 220 
 221     fn verifySymbols(op: *Operation) SymbolTableError!void {
 222         if (!isSymbolTableOperation(op)) return error.InvalidSymbolTable;
 223         if (op.regions.items.len != 1) return error.InvalidSymbolTable;
 224         const region = &op.regions.items[0];
 225         if (region.blocks.size != 1) return error.InvalidSymbolTable;
 226         const block = region.blocks.front() orelse return;
 227         var ops = block.getOperations();
 228         while (ops.next()) |candidate| {
 229             if (!isSymbolOperation(candidate)) continue;
 230             if (isDeclaration(candidate) and getSymbolVisibility(candidate) == .public) {
 231                 return error.InvalidSymbolDeclaration;
 232             }
 233         }
 234     }
 235 
 236     pub fn lookupSymbolIn(op: *Operation, name: []const u8) ?*Operation {
 237         if (!isSymbolTableOperation(op)) return null;
 238         if (op.regions.items.len == 0) return null;
 239         const region = &op.regions.items[0];
 240         const block = region.blocks.front() orelse return null;
 241         var ops = block.getOperations();
 242         while (ops.next()) |candidate| {
 243             if (getSymbolName(candidate)) |symbol_name| {
 244                 if (std.mem.eql(u8, symbol_name, name)) return candidate;
 245             }
 246         }
 247         return null;
 248     }
 249 
 250     pub fn lookupSymbolRefIn(op: *Operation, symbol_ref: *const Attribute.SymbolRefAttr) ?*Operation {
 251         var resolved = lookupSymbolIn(op, symbol_ref.getRootReference()) orelse return null;
 252         for (symbol_ref.getNestedReferences()) |nested_ref| {
 253             resolved = lookupSymbolIn(resolved, nested_ref) orelse return null;
 254             if (getSymbolVisibility(resolved) == .private) return null;
 255         }
 256         return resolved;
 257     }
 258 
 259     pub fn getNearestSymbolTable(from: *Operation) ?*Operation {
 260         var current: ?*Operation = from;
 261         while (current) |op| {
 262             if (isSymbolTableOperation(op)) return op;
 263             current = op.getParentOp();
 264         }
 265         return null;
 266     }
 267 
 268     pub fn lookupNearestSymbolFrom(from: *Operation, name: []const u8) ?*Operation {
 269         const table_op = getNearestSymbolTable(from) orelse return null;
 270         return lookupSymbolIn(table_op, name);
 271     }
 272 
 273     pub fn lookupNearestSymbolRefFrom(from: *Operation, symbol_ref: *const Attribute.SymbolRefAttr) ?*Operation {
 274         const table_op = getNearestSymbolTable(from) orelse return null;
 275         return lookupSymbolRefIn(table_op, symbol_ref);
 276     }
 277 
 278     pub fn collectSymbolUses(
 279         allocator: std.mem.Allocator,
 280         from: *Operation,
 281     ) anyerror!SymbolUseRange {
 282         var range = SymbolUseRange{};
 283         errdefer range.deinit(allocator);
 284         try appendSymbolUsesFromOperation(allocator, &range, from, null);
 285         return range;
 286     }
 287 
 288     pub fn collectSymbolUsesInRegion(
 289         allocator: std.mem.Allocator,
 290         from: *Region,
 291     ) anyerror!SymbolUseRange {
 292         var range = SymbolUseRange{};
 293         errdefer range.deinit(allocator);
 294         try appendSymbolUsesFromRegion(allocator, &range, from, null);
 295         return range;
 296     }
 297 
 298     pub fn collectSymbolUsesInSymbolTable(
 299         allocator: std.mem.Allocator,
 300         op: *Operation,
 301     ) anyerror!SymbolUseRange {
 302         if (!isSymbolTableOperation(op)) return error.InvalidSymbolTable;
 303         var range = SymbolUseRange{};
 304         errdefer range.deinit(allocator);
 305         for (op.regions.items) |*region| {
 306             try appendSymbolUsesFromRegion(allocator, &range, region, null);
 307         }
 308         return range;
 309     }
 310 
 311     pub fn collectSymbolUsesOf(
 312         allocator: std.mem.Allocator,
 313         from: *Operation,
 314         symbol_ref: *const Attribute.SymbolRefAttr,
 315     ) anyerror!SymbolUseRange {
 316         var range = SymbolUseRange{};
 317         errdefer range.deinit(allocator);
 318         try appendSymbolUsesFromOperation(allocator, &range, from, symbol_ref);
 319         return range;
 320     }
 321 
 322     pub fn collectSymbolUsesOfInRegion(
 323         allocator: std.mem.Allocator,
 324         from: *Region,
 325         symbol_ref: *const Attribute.SymbolRefAttr,
 326     ) anyerror!SymbolUseRange {
 327         var range = SymbolUseRange{};
 328         errdefer range.deinit(allocator);
 329         try appendSymbolUsesFromRegion(allocator, &range, from, symbol_ref);
 330         return range;
 331     }
 332 
 333     pub fn collectSymbolUsesOfInSymbolTable(
 334         allocator: std.mem.Allocator,
 335         op: *Operation,
 336         symbol_ref: *const Attribute.SymbolRefAttr,
 337     ) anyerror!SymbolUseRange {
 338         if (!isSymbolTableOperation(op)) return error.InvalidSymbolTable;
 339         var range = SymbolUseRange{};
 340         errdefer range.deinit(allocator);
 341         for (op.regions.items) |*region| {
 342             try appendSymbolUsesFromRegion(allocator, &range, region, symbol_ref);
 343         }
 344         return range;
 345     }
 346 
 347     pub fn symbolKnownUseEmpty(
 348         allocator: std.mem.Allocator,
 349         from: *Operation,
 350         symbol_ref: *const Attribute.SymbolRefAttr,
 351     ) anyerror!bool {
 352         var uses = try collectSymbolUsesOf(allocator, from, symbol_ref);
 353         defer uses.deinit(allocator);
 354         return uses.empty();
 355     }
 356 
 357     pub fn symbolKnownUseEmptyInRegion(
 358         allocator: std.mem.Allocator,
 359         from: *Region,
 360         symbol_ref: *const Attribute.SymbolRefAttr,
 361     ) anyerror!bool {
 362         var uses = try collectSymbolUsesOfInRegion(allocator, from, symbol_ref);
 363         defer uses.deinit(allocator);
 364         return uses.empty();
 365     }
 366 
 367     pub fn symbolKnownUseEmptyInSymbolTable(
 368         allocator: std.mem.Allocator,
 369         op: *Operation,
 370         symbol_ref: *const Attribute.SymbolRefAttr,
 371     ) anyerror!bool {
 372         var uses = try collectSymbolUsesOfInSymbolTable(allocator, op, symbol_ref);
 373         defer uses.deinit(allocator);
 374         return uses.empty();
 375     }
 376 
 377     pub fn replaceAllSymbolUsesInRegion(
 378         allocator: std.mem.Allocator,
 379         from: *Region,
 380         old_ref: *const Attribute.SymbolRefAttr,
 381         new_leaf: []const u8,
 382     ) anyerror!usize {
 383         return replaceSymbolUsesInRegion(allocator, from, old_ref, new_leaf);
 384     }
 385 
 386     pub fn replaceAllSymbolUsesInSymbolTable(
 387         allocator: std.mem.Allocator,
 388         op: *Operation,
 389         old_ref: *const Attribute.SymbolRefAttr,
 390         new_leaf: []const u8,
 391     ) anyerror!usize {
 392         if (!isSymbolTableOperation(op)) return error.InvalidSymbolTable;
 393         var replaced: usize = 0;
 394         for (op.regions.items) |*region| {
 395             replaced += try replaceSymbolUsesInRegion(allocator, region, old_ref, new_leaf);
 396         }
 397         return replaced;
 398     }
 399 
 400     pub fn renameSymbolInSymbolTable(
 401         allocator: std.mem.Allocator,
 402         op: *Operation,
 403         symbol: *Operation,
 404         new_name: []const u8,
 405     ) anyerror!usize {
 406         if (!isSymbolTableOperation(op)) return error.InvalidSymbolTable;
 407         if (symbol.getParentOp() != op) return error.InvalidSymbolTable;
 408         const old_name = getSymbolName(symbol) orelse return error.InvalidSymbol;
 409         if (std.mem.eql(u8, old_name, new_name)) return 0;
 410         if (lookupSymbolIn(op, new_name)) |existing| {
 411             if (existing != symbol) return error.DuplicateSymbol;
 412         }
 413 
 414         var symbol_tables = Collection.init(allocator);
 415         defer symbol_tables.deinit();
 416         var users = try SymbolUserMap.init(allocator, &symbol_tables, op);
 417         defer users.deinit();
 418         const replaced = try users.replaceAllUsesWith(symbol, new_name);
 419         try setSymbolName(symbol, new_name);
 420         symbol_tables.invalidateSymbolTable(op);
 421         return replaced;
 422     }
 423 
 424     pub fn getSymbolName(op: *Operation) ?[]const u8 {
 425         const iface = op.interface(interfaces.SymbolOpInterface) orelse return null;
 426         return iface.call(.getSymbolName, .{});
 427     }
 428 
 429     pub fn setSymbolName(op: *Operation, name: []const u8) anyerror!void {
 430         const iface = op.interface(interfaces.SymbolOpInterface) orelse return error.InvalidSymbol;
 431         try iface.call(.setSymbolName, .{name});
 432     }
 433 
 434     pub fn isDeclaration(op: *Operation) bool {
 435         const iface = op.interface(interfaces.SymbolOpInterface) orelse return false;
 436         return iface.call(.isDeclaration, .{});
 437     }
 438 
 439     pub fn getSymbolVisibility(op: *const Operation) Visibility {
 440         const string_attr = op.getAttrAs(Attribute.StringAttr, symbol_attr_names.sym_visibility) orelse return .public;
 441         return Visibility.fromString(string_attr.getValue()) orelse .public;
 442     }
 443 
 444     pub fn setSymbolVisibility(op: *Operation, visibility: Visibility) anyerror!void {
 445         if (!isSymbolOperation(op)) return error.InvalidSymbol;
 446         if (visibility == .public) {
 447             if (isDeclaration(op)) return error.InvalidSymbolDeclaration;
 448             _ = op.removeAttr(symbol_attr_names.sym_visibility);
 449             return;
 450         }
 451         try op.setAttr(
 452             symbol_attr_names.sym_visibility,
 453             try op.getContext().getStringAttr(visibility.toString()),
 454         );
 455     }
 456 
 457     fn appendSymbolRefsFromOperation(
 458         allocator: std.mem.Allocator,
 459         range: *SymbolUseRange,
 460         op: *Operation,
 461         target_ref: ?*const Attribute.SymbolRefAttr,
 462     ) anyerror!void {
 463         var attrs = op.getAttrs();
 464         while (attrs.next()) |attr| {
 465             try appendSymbolRefsFromAttribute(allocator, range, op, attr.name, attr.value, target_ref);
 466         }
 467     }
 468 
 469     fn appendSymbolRefsFromAttribute(
 470         allocator: std.mem.Allocator,
 471         range: *SymbolUseRange,
 472         op: *Operation,
 473         attr_name: []const u8,
 474         attr: Attribute,
 475         target_ref: ?*const Attribute.SymbolRefAttr,
 476     ) anyerror!void {
 477         if (getSymbolRefAttr(attr)) |symbol_ref| {
 478             if (target_ref) |target| {
 479                 if (!isReferencePrefixOf(target, symbol_ref)) return;
 480             }
 481             try range.append(allocator, .{
 482                 .user = op,
 483                 .attr_name = attr_name,
 484                 .symbol_ref = symbol_ref,
 485             });
 486             return;
 487         }
 488 
 489         const array_attr = getArrayAttr(attr) orelse return;
 490         for (array_attr.values) |child| {
 491             try appendSymbolRefsFromAttribute(allocator, range, op, attr_name, child, target_ref);
 492         }
 493     }
 494 
 495     fn appendSymbolUsesFromOperation(
 496         allocator: std.mem.Allocator,
 497         range: *SymbolUseRange,
 498         op: *Operation,
 499         target_ref: ?*const Attribute.SymbolRefAttr,
 500     ) anyerror!void {
 501         var state = SymbolUseWalkState{
 502             .allocator = allocator,
 503             .range = range,
 504             .target_ref = target_ref,
 505         };
 506         _ = try op.walk(.{ .order = .pre_order }, &state, SymbolUseWalkState.visit);
 507     }
 508 
 509     fn verifySymbolUsesInSymbolTable(symbol_tables: *Collection, op: *Operation) anyerror!void {
 510         if (!isSymbolTableOperation(op)) return error.InvalidSymbolTable;
 511         for (op.regions.items) |*region| {
 512             try verifySymbolUsesInRegion(symbol_tables, region);
 513         }
 514     }
 515 
 516     fn verifySymbolUsesFromOperation(symbol_tables: *Collection, op: *Operation) anyerror!void {
 517         var state = VerifySymbolUsesWalkState{ .symbol_tables = symbol_tables };
 518         _ = try op.walk(.{ .order = .pre_order }, &state, VerifySymbolUsesWalkState.visit);
 519     }
 520 
 521     fn verifySymbolUsesOnOperation(symbol_tables: *Collection, op: *Operation) anyerror!void {
 522         if (op.interface(interfaces.SymbolUserOpInterface)) |iface| {
 523             try iface.call(.verifySymbolUses, .{symbol_tables});
 524         }
 525         var attrs = op.getDiscardableAttrs();
 526         while (attrs.next()) |attr| {
 527             if (attr.value.interface(interfaces.SymbolUserAttrInterface)) |iface| {
 528                 try iface.call(.verifySymbolUses, .{ op, symbol_tables });
 529             }
 530         }
 531     }
 532 
 533     fn verifySymbolUsesInRegion(symbol_tables: *Collection, region: *Region) anyerror!void {
 534         var state = VerifySymbolUsesWalkState{ .symbol_tables = symbol_tables };
 535         _ = try region.walkOperations(.{ .order = .pre_order }, &state, VerifySymbolUsesWalkState.visit);
 536     }
 537 
 538     fn appendSymbolUsesFromRegion(
 539         allocator: std.mem.Allocator,
 540         range: *SymbolUseRange,
 541         region: *Region,
 542         target_ref: ?*const Attribute.SymbolRefAttr,
 543     ) anyerror!void {
 544         var state = SymbolUseWalkState{
 545             .allocator = allocator,
 546             .range = range,
 547             .target_ref = target_ref,
 548         };
 549         _ = try region.walkOperations(.{ .order = .pre_order }, &state, SymbolUseWalkState.visit);
 550     }
 551 
 552     fn replaceSymbolUsesOnOperation(
 553         allocator: std.mem.Allocator,
 554         op: *Operation,
 555         old_ref: *const Attribute.SymbolRefAttr,
 556         new_leaf: []const u8,
 557     ) anyerror!usize {
 558         var replaced: usize = 0;
 559         var inline_snapshot: [4]NamedAttribute = undefined;
 560         const attr_count = op.getNumAttrs();
 561         const attr_snapshot = if (attr_count <= inline_snapshot.len)
 562             inline_snapshot[0..attr_count]
 563         else
 564             try allocator.alloc(NamedAttribute, attr_count);
 565         defer if (attr_count > inline_snapshot.len) allocator.free(attr_snapshot);
 566         var attrs = op.getAttrs();
 567         var attr_index: usize = 0;
 568         while (attrs.next()) |attr| : (attr_index += 1) {
 569             if (attr_index >= attr_snapshot.len) return error.OperationAttributesChanged;
 570             attr_snapshot[attr_index] = attr;
 571         }
 572         if (attr_index != attr_snapshot.len) return error.OperationAttributesChanged;
 573         for (attr_snapshot) |attr| {
 574             const result = try replaceSymbolUsesInAttribute(allocator, op, old_ref, attr.value, new_leaf);
 575             const replacement = result.attr orelse continue;
 576             try op.setAttr(attr.name, replacement);
 577             replaced += result.replaced;
 578         }
 579         return replaced;
 580     }
 581 
 582     fn replaceSymbolUsesInAttribute(
 583         allocator: std.mem.Allocator,
 584         op: *Operation,
 585         old_ref: *const Attribute.SymbolRefAttr,
 586         attr: Attribute,
 587         new_leaf: []const u8,
 588     ) anyerror!AttributeReplacement {
 589         if (getSymbolRefAttr(attr)) |symbol_ref| {
 590             const replacement = try replacementSymbolRefAttr(allocator, op, old_ref, symbol_ref, new_leaf) orelse return .{};
 591             return .{ .attr = replacement, .replaced = 1 };
 592         }
 593 
 594         const array_attr = getArrayAttr(attr) orelse return .{};
 595         var changed = false;
 596         var replaced: usize = 0;
 597         const values = try allocator.alloc(Attribute, array_attr.values.len);
 598         defer allocator.free(values);
 599         for (array_attr.values, 0..) |child, index| {
 600             const result = try replaceSymbolUsesInAttribute(allocator, op, old_ref, child, new_leaf);
 601             values[index] = result.attr orelse child;
 602             if (result.attr != null) changed = true;
 603             replaced += result.replaced;
 604         }
 605         if (!changed) return .{};
 606         return .{
 607             .attr = try op.getContext().getArrayAttr(values),
 608             .replaced = replaced,
 609         };
 610     }
 611 
 612     fn replaceSymbolUsesInOperation(
 613         allocator: std.mem.Allocator,
 614         op: *Operation,
 615         old_ref: *const Attribute.SymbolRefAttr,
 616         new_leaf: []const u8,
 617     ) anyerror!usize {
 618         var state = ReplaceSymbolUsesWalkState{
 619             .allocator = allocator,
 620             .old_ref = old_ref,
 621             .new_leaf = new_leaf,
 622         };
 623         _ = try op.walk(.{ .order = .pre_order }, &state, ReplaceSymbolUsesWalkState.visit);
 624         return state.replaced;
 625     }
 626 
 627     fn replaceSymbolUsesInRegion(
 628         allocator: std.mem.Allocator,
 629         region: *Region,
 630         old_ref: *const Attribute.SymbolRefAttr,
 631         new_leaf: []const u8,
 632     ) anyerror!usize {
 633         var state = ReplaceSymbolUsesWalkState{
 634             .allocator = allocator,
 635             .old_ref = old_ref,
 636             .new_leaf = new_leaf,
 637         };
 638         _ = try region.walkOperations(.{ .order = .pre_order }, &state, ReplaceSymbolUsesWalkState.visit);
 639         return state.replaced;
 640     }
 641 
 642     fn replacementSymbolRefAttr(
 643         allocator: std.mem.Allocator,
 644         op: *Operation,
 645         old_ref: *const Attribute.SymbolRefAttr,
 646         current_ref: *const Attribute.SymbolRefAttr,
 647         new_leaf: []const u8,
 648     ) anyerror!?Attribute {
 649         if (!isReferencePrefixOf(old_ref, current_ref)) return null;
 650         if (current_ref.getNestedReferences().len == 0) {
 651             return try op.getContext().getFlatSymbolRefAttr(new_leaf);
 652         }
 653         if (old_ref.getNestedReferences().len == 0) {
 654             return try op.getContext().getSymbolRefAttr(new_leaf, current_ref.getNestedReferences());
 655         }
 656 
 657         const nested = try allocator.dupe([]const u8, current_ref.getNestedReferences());
 658         defer allocator.free(nested);
 659         nested[old_ref.getNestedReferences().len - 1] = new_leaf;
 660         return try op.getContext().getSymbolRefAttr(current_ref.getRootReference(), nested);
 661     }
 662 
 663     fn symbolUsePrefix(use: SymbolUse, nested_len: usize) anyerror!SymbolUse {
 664         const attr = if (nested_len == 0)
 665             try use.user.getContext().getFlatSymbolRefAttr(use.symbol_ref.getRootReference())
 666         else
 667             try use.user.getContext().getSymbolRefAttr(
 668                 use.symbol_ref.getRootReference(),
 669                 use.symbol_ref.getNestedReferences()[0..nested_len],
 670             );
 671         const symbol_ref = attr.cast(Attribute.SymbolRefAttr).?;
 672         return .{
 673             .user = use.user,
 674             .attr_name = use.attr_name,
 675             .symbol_ref = symbol_ref,
 676         };
 677     }
 678 
 679     fn sameUse(a: SymbolUse, b: SymbolUse) bool {
 680         return a.user == b.user and
 681             std.mem.eql(u8, a.attr_name, b.attr_name) and
 682             symbolRefsEqual(a.symbol_ref, b.symbol_ref);
 683     }
 684 
 685     fn symbolRefsEqual(a: *const Attribute.SymbolRefAttr, b: *const Attribute.SymbolRefAttr) bool {
 686         if (a == b) return true;
 687         if (!std.mem.eql(u8, a.getRootReference(), b.getRootReference())) return false;
 688         const a_nested = a.getNestedReferences();
 689         const b_nested = b.getNestedReferences();
 690         if (a_nested.len != b_nested.len) return false;
 691         for (a_nested, b_nested) |a_ref, b_ref| {
 692             if (!std.mem.eql(u8, a_ref, b_ref)) return false;
 693         }
 694         return true;
 695     }
 696 
 697     fn isReferencePrefixOf(prefix: *const Attribute.SymbolRefAttr, symbol_ref: *const Attribute.SymbolRefAttr) bool {
 698         if (!std.mem.eql(u8, prefix.getRootReference(), symbol_ref.getRootReference())) return false;
 699         const prefix_nested = prefix.getNestedReferences();
 700         const ref_nested = symbol_ref.getNestedReferences();
 701         if (prefix_nested.len > ref_nested.len) return false;
 702         for (prefix_nested, 0..) |nested, index| {
 703             if (!std.mem.eql(u8, nested, ref_nested[index])) return false;
 704         }
 705         return true;
 706     }
 707 
 708     fn getSymbolRefAttr(attr: Attribute) ?*const Attribute.SymbolRefAttr {
 709         if (!std.mem.eql(u8, attr.abstract.name, attribute.builtin_attr_names.symbol_ref)) return null;
 710         return attr.cast(Attribute.SymbolRefAttr);
 711     }
 712 
 713     fn getArrayAttr(attr: Attribute) ?*const Attribute.ArrayAttr {
 714         if (!std.mem.eql(u8, attr.abstract.name, attribute.builtin_attr_names.array)) return null;
 715         return attr.cast(Attribute.ArrayAttr);
 716     }
 717 };
 718 
 719 const SymbolUserSet = struct {
 720     users: std.ArrayListUnmanaged(*Operation) = .empty,
 721     uses: std.ArrayListUnmanaged(SymbolUse) = .empty,
 722 
 723     fn deinit(self: *SymbolUserSet, allocator: std.mem.Allocator) void {
 724         self.users.deinit(allocator);
 725         self.uses.deinit(allocator);
 726         self.* = .{};
 727     }
 728 
 729     fn append(self: *SymbolUserSet, allocator: std.mem.Allocator, use: SymbolUse) !void {
 730         var found_user = false;
 731         for (self.users.items) |user| {
 732             if (user == use.user) {
 733                 found_user = true;
 734                 break;
 735             }
 736         }
 737         if (!found_user) try self.users.append(allocator, use.user);
 738 
 739         for (self.uses.items) |existing| {
 740             if (SymbolTable.sameUse(existing, use)) return;
 741         }
 742         try self.uses.append(allocator, use);
 743     }
 744 
 745     fn appendAll(
 746         self: *SymbolUserSet,
 747         allocator: std.mem.Allocator,
 748         other: *const SymbolUserSet,
 749     ) !void {
 750         for (other.uses.items) |use| {
 751             try self.append(allocator, use);
 752         }
 753     }
 754 };
 755 
 756 pub const SymbolUserMap = struct {
 757     allocator: std.mem.Allocator,
 758     symbol_tables: *SymbolTable.Collection,
 759     symbol_users: std.AutoArrayHashMapUnmanaged(*Operation, SymbolUserSet) = .empty,
 760 
 761     pub fn init(
 762         allocator: std.mem.Allocator,
 763         symbol_tables: *SymbolTable.Collection,
 764         symbol_table_op: *Operation,
 765     ) anyerror!SymbolUserMap {
 766         if (!SymbolTable.isSymbolTableOperation(symbol_table_op)) return error.InvalidSymbolTable;
 767         var self = SymbolUserMap{
 768             .allocator = allocator,
 769             .symbol_tables = symbol_tables,
 770         };
 771         errdefer self.deinit();
 772         try self.appendFromSymbolTable(symbol_table_op);
 773         return self;
 774     }
 775 
 776     pub fn deinit(self: *SymbolUserMap) void {
 777         for (self.symbol_users.values()) |*users| {
 778             users.deinit(self.allocator);
 779         }
 780         self.symbol_users.deinit(self.allocator);
 781         self.symbol_users = .empty;
 782     }
 783 
 784     pub fn getUsers(self: *SymbolUserMap, symbol: *Operation) []const *Operation {
 785         if (self.symbol_users.getPtr(symbol)) |users| {
 786             return users.users.items;
 787         }
 788         return &.{};
 789     }
 790 
 791     pub fn useEmpty(self: *SymbolUserMap, symbol: *Operation) bool {
 792         return self.getUsers(symbol).len == 0;
 793     }
 794 
 795     pub fn replaceAllUsesWith(
 796         self: *SymbolUserMap,
 797         symbol: *Operation,
 798         new_name: []const u8,
 799     ) anyerror!usize {
 800         const old_name = SymbolTable.getSymbolName(symbol) orelse return error.InvalidSymbol;
 801         if (std.mem.eql(u8, old_name, new_name)) return 0;
 802 
 803         const users = self.symbol_users.getPtr(symbol) orelse return 0;
 804         var replaced: usize = 0;
 805         for (users.uses.items) |*use| {
 806             const replacement = try SymbolTable.replacementSymbolRefAttr(self.allocator, use.user, use.symbol_ref, use.symbol_ref, new_name) orelse continue;
 807             const replacement_ref = replacement.cast(Attribute.SymbolRefAttr).?;
 808             replaced += try SymbolTable.replaceSymbolUsesOnOperation(self.allocator, use.user, use.symbol_ref, new_name);
 809             use.symbol_ref = replacement_ref;
 810         }
 811 
 812         const new_symbol = blk: {
 813             const parent = symbol.getParentOp() orelse break :blk null;
 814             break :blk try self.symbol_tables.lookupSymbolIn(parent, new_name);
 815         };
 816         try self.moveUsers(symbol, new_symbol);
 817         return replaced;
 818     }
 819 
 820     fn appendFromSymbolTable(self: *SymbolUserMap, symbol_table_op: *Operation) anyerror!void {
 821         var uses = try SymbolTable.collectSymbolUsesInSymbolTable(self.allocator, symbol_table_op);
 822         defer uses.deinit(self.allocator);
 823 
 824         for (uses.items()) |use| {
 825             try self.appendResolvedUse(symbol_table_op, use);
 826         }
 827 
 828         try self.appendNestedSymbolTables(symbol_table_op);
 829     }
 830 
 831     fn appendNestedSymbolTables(self: *SymbolUserMap, op: *Operation) anyerror!void {
 832         var state = NestedSymbolTableWalkState{ .user_map = self };
 833         for (op.regions.items) |*region| {
 834             _ = try region.walkOperations(.{ .order = .pre_order }, &state, NestedSymbolTableWalkState.visit);
 835         }
 836     }
 837 
 838     fn appendResolvedUse(
 839         self: *SymbolUserMap,
 840         symbol_table_op: *Operation,
 841         use: SymbolUse,
 842     ) anyerror!void {
 843         var resolved = try self.symbol_tables.lookupSymbolIn(symbol_table_op, use.symbol_ref.getRootReference()) orelse return;
 844         try self.appendSymbolUse(resolved, try SymbolTable.symbolUsePrefix(use, 0));
 845 
 846         for (use.symbol_ref.getNestedReferences(), 0..) |nested_ref, index| {
 847             resolved = try self.symbol_tables.lookupSymbolIn(resolved, nested_ref) orelse return;
 848             if (SymbolTable.getSymbolVisibility(resolved) == .private) return;
 849             try self.appendSymbolUse(resolved, try SymbolTable.symbolUsePrefix(use, index + 1));
 850         }
 851     }
 852 
 853     fn appendSymbolUse(self: *SymbolUserMap, symbol: *Operation, use: SymbolUse) !void {
 854         const entry = try self.symbol_users.getOrPut(self.allocator, symbol);
 855         if (!entry.found_existing) entry.value_ptr.* = .{};
 856         try entry.value_ptr.append(self.allocator, use);
 857     }
 858 
 859     fn moveUsers(self: *SymbolUserMap, old_symbol: *Operation, new_symbol: ?*Operation) !void {
 860         var old_users = self.symbol_users.getPtr(old_symbol).?.*;
 861         _ = self.symbol_users.swapRemove(old_symbol);
 862         var transferred = false;
 863         errdefer if (!transferred) old_users.deinit(self.allocator);
 864 
 865         if (new_symbol) |target| {
 866             if (self.symbol_users.getPtr(target)) |target_users| {
 867                 try target_users.appendAll(self.allocator, &old_users);
 868                 old_users.deinit(self.allocator);
 869                 transferred = true;
 870                 return;
 871             }
 872 
 873             const entry = try self.symbol_users.getOrPut(self.allocator, target);
 874             entry.value_ptr.* = old_users;
 875             transferred = true;
 876             return;
 877         }
 878 
 879         old_users.deinit(self.allocator);
 880         transferred = true;
 881     }
 882 };
 883 
 884 const NestedSymbolTableWalkState = struct {
 885     user_map: *SymbolUserMap,
 886 
 887     fn visit(self: *@This(), op: *Operation) anyerror!Operation.WalkResult {
 888         if (!SymbolTable.isSymbolTableOperation(op)) return .advance;
 889         try self.user_map.appendFromSymbolTable(op);
 890         return .skip;
 891     }
 892 };
 893 
 894 const SymbolUseWalkState = struct {
 895     allocator: std.mem.Allocator,
 896     range: *SymbolUseRange,
 897     target_ref: ?*const Attribute.SymbolRefAttr,
 898 
 899     fn visit(self: *@This(), op: *Operation) anyerror!Operation.WalkResult {
 900         try SymbolTable.appendSymbolRefsFromOperation(self.allocator, self.range, op, self.target_ref);
 901         if (SymbolTable.isSymbolTableOperation(op)) return .skip;
 902         return .advance;
 903     }
 904 };
 905 
 906 const VerifySymbolUsesWalkState = struct {
 907     symbol_tables: *SymbolTable.Collection,
 908 
 909     fn visit(self: *@This(), op: *Operation) anyerror!Operation.WalkResult {
 910         try SymbolTable.verifySymbolUsesOnOperation(self.symbol_tables, op);
 911         if (SymbolTable.isSymbolTableOperation(op)) return .skip;
 912         return .advance;
 913     }
 914 };
 915 
 916 const ReplaceSymbolUsesWalkState = struct {
 917     allocator: std.mem.Allocator,
 918     old_ref: *const Attribute.SymbolRefAttr,
 919     new_leaf: []const u8,
 920     replaced: usize = 0,
 921 
 922     fn visit(self: *@This(), op: *Operation) anyerror!Operation.WalkResult {
 923         self.replaced += try SymbolTable.replaceSymbolUsesOnOperation(self.allocator, op, self.old_ref, self.new_leaf);
 924         if (SymbolTable.isSymbolTableOperation(op)) return .skip;
 925         return .advance;
 926     }
 927 };
 928 
 929 const TestModuleSymbol = struct {
 930     fn getSymbolName(op_ptr: *const anyopaque) ?[]const u8 {
 931         const op: *const Operation = @ptrCast(@alignCast(op_ptr));
 932         const attr = op.getAttr("sym_name") orelse return null;
 933         if (!std.mem.eql(u8, attr.abstract.name, "test.string")) return null;
 934         const dialect_attr = attr.cast(Attribute.DialectAttr) orelse return null;
 935         return dialect_attr.payload;
 936     }
 937 
 938     fn setSymbolName(op_ptr: *const anyopaque, name: []const u8) anyerror!void {
 939         const op: *Operation = @ptrCast(@alignCast(@constCast(op_ptr)));
 940         try op.setAttr("sym_name", try op.getContext().getDialectAttr("test.string", name));
 941     }
 942 
 943     fn isDeclaration(_: *const anyopaque) bool {
 944         return false;
 945     }
 946 
 947     const vtable = interfaces.SymbolOpInterface.VTable{
 948         .getSymbolName = getSymbolName,
 949         .setSymbolName = setSymbolName,
 950         .isDeclaration = isDeclaration,
 951     };
 952 };
 953 
 954 const TestSymbolUserAttribute = struct {
 955     fn verifySymbolUses(
 956         attr_ptr: *const anyopaque,
 957         op: *Operation,
 958         symbol_tables: *SymbolTable.Collection,
 959     ) anyerror!void {
 960         const attr: *const Attribute.DialectAttr = @ptrCast(@alignCast(attr_ptr));
 961         _ = try symbol_tables.lookupNearestSymbolFrom(op, attr.payload) orelse return error.UnresolvedAttrSymbol;
 962     }
 963 };
 964 
 965 const ReadOnlySymbolRefProperties = struct {
 966     value: ?Attribute = null,
 967 
 968     fn from(storage: *anyopaque) *@This() {
 969         return @ptrCast(@alignCast(storage));
 970     }
 971 
 972     fn fromConst(storage: *const anyopaque) *const @This() {
 973         return @ptrCast(@alignCast(storage));
 974     }
 975 
 976     fn init(storage: *anyopaque, _: std.mem.Allocator) anyerror!void {
 977         from(storage).* = .{};
 978     }
 979 
 980     fn deinit(_: *anyopaque, _: std.mem.Allocator) void {}
 981 
 982     fn get(_: *const Operation, storage: *const anyopaque, name: []const u8) ?Attribute {
 983         if (!std.mem.eql(u8, name, "callee")) return null;
 984         return fromConst(storage).value;
 985     }
 986 
 987     fn getProperties(_: *const Operation, storage: *const anyopaque) ?Attribute {
 988         return fromConst(storage).value;
 989     }
 990 
 991     fn setProperties(_: *Operation, storage: *anyopaque, attr: Attribute) anyerror!void {
 992         from(storage).value = attr;
 993     }
 994 
 995     fn copyProperties(dest: *anyopaque, source: *const anyopaque) anyerror!void {
 996         from(dest).* = fromConst(source).*;
 997     }
 998 
 999     const model = interfaces.OperationPropertiesModel{
1000         .name = "test.read_only_symbol_ref.properties",
1001         .size = @sizeOf(@This()),
1002         .alignment = std.mem.Alignment.fromByteUnits(@alignOf(@This())),
1003         .init = init,
1004         .deinit = deinit,
1005         .getInherentAttr = get,
1006         .getPropertiesAsAttr = getProperties,
1007         .setPropertiesFromAttr = setProperties,
1008         .copyProperties = copyProperties,
1009     };
1010 };
1011 
1012 test "SymbolTable collects symbol ops in a region" {
1013     const testing = std.testing;
1014     var arena = alloc_arena.Arena.init(std.testing.allocator);
1015     defer arena.deinit();
1016     const allocator = arena.allocator();
1017 
1018     const test_dialect = @import("../dialects/fixture/root.zig");
1019     const Location = @import("location.zig").Location;
1020 
1021     const Context = @import("context/root.zig").Context;
1022     var ctx = try Context.init(allocator, Context.Limits.testing);
1023     defer ctx.deinit(allocator);
1024     try ctx.allowUnregistered();
1025     try test_dialect.registerTestDialect(&ctx);
1026 
1027     const loc = Location.getUnknown();
1028     const module = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
1029     const block = module.getBodyBlock();
1030 
1031     const f1 = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "foo", &.{});
1032     const f2 = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "bar", &.{});
1033     try block.addOperation(f1.op);
1034     try block.addOperation(f2.op);
1035 
1036     var table = SymbolTable.init(allocator);
1037     defer table.deinit();
1038 
1039     try table.buildFromOperation(module.op);
1040 
1041     try testing.expect(table.lookup("foo") == f1.op);
1042     try testing.expect(table.lookup("bar") == f2.op);
1043 }
1044 
1045 test "SymbolTable ignores sym_name attributes on non-symbol operations" {
1046     const testing = std.testing;
1047     var arena = alloc_arena.Arena.init(std.testing.allocator);
1048     defer arena.deinit();
1049     const allocator = arena.allocator();
1050 
1051     const test_dialect = @import("../dialects/fixture/root.zig");
1052     const Location = @import("location.zig").Location;
1053 
1054     const Context = @import("context/root.zig").Context;
1055     var ctx = try Context.init(allocator, Context.Limits.testing);
1056     defer ctx.deinit(allocator);
1057     try ctx.allowUnregistered();
1058     try test_dialect.registerTestDialect(&ctx);
1059 
1060     const loc = Location.getUnknown();
1061     const module = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
1062     const block = module.getBodyBlock();
1063 
1064     const symbol = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "target", &.{});
1065     try block.addOperation(symbol.op);
1066 
1067     const plain = try ctx.createOperation(Operation.State.init("test.plain", loc));
1068     try plain.setAttr(SymbolTable.symbol_attr_names.sym_name, try ctx.getStringAttr("target"));
1069     try block.addOperation(plain);
1070 
1071     var table = SymbolTable.init(allocator);
1072     defer table.deinit();
1073 
1074     try table.buildFromOperation(module.op);
1075 
1076     try testing.expect(table.lookup("target") == symbol.op);
1077     try testing.expect(!SymbolTable.isSymbolOperation(plain));
1078     try testing.expect(SymbolTable.getSymbolName(plain) == null);
1079     try testing.expectError(error.InvalidSymbol, SymbolTable.setSymbolName(plain, "renamed"));
1080     try testing.expectError(error.InvalidSymbol, SymbolTable.setSymbolVisibility(plain, .private));
1081 }
1082 
1083 test "SymbolTable rejects duplicate symbols" {
1084     const testing = std.testing;
1085     var arena = alloc_arena.Arena.init(std.testing.allocator);
1086     defer arena.deinit();
1087     const allocator = arena.allocator();
1088 
1089     const test_dialect = @import("../dialects/fixture/root.zig");
1090     const Location = @import("location.zig").Location;
1091 
1092     const Context = @import("context/root.zig").Context;
1093     var ctx = try Context.init(allocator, Context.Limits.testing);
1094     defer ctx.deinit(allocator);
1095     try ctx.allowUnregistered();
1096     try test_dialect.registerTestDialect(&ctx);
1097 
1098     const loc = Location.getUnknown();
1099     const module = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
1100     const block = module.getBodyBlock();
1101 
1102     const f1 = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "dup", &.{});
1103     const f2 = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "dup", &.{});
1104     try block.addOperation(f1.op);
1105     try block.addOperation(f2.op);
1106 
1107     var table = SymbolTable.init(allocator);
1108     defer table.deinit();
1109 
1110     try testing.expectError(error.DuplicateSymbol, table.buildFromOperation(module.op));
1111     try testing.expectError(error.DuplicateSymbol, @import("verify.zig").verifyOperation(module.op, @import("verify.zig").default_options));
1112 }
1113 
1114 test "SymbolTable rejects public declarations" {
1115     const testing = std.testing;
1116     var arena = alloc_arena.Arena.init(std.testing.allocator);
1117     defer arena.deinit();
1118     const allocator = arena.allocator();
1119 
1120     const dialects = @import("../dialects/root.zig");
1121     const core_dialects = @import("root.zig").dialects;
1122     const Location = @import("location.zig").Location;
1123 
1124     const Context = @import("context/root.zig").Context;
1125     var ctx = try Context.init(allocator, Context.Limits.testing);
1126     defer ctx.deinit(allocator);
1127     try ctx.allowUnregistered();
1128     try core_dialects.loadDialectSpec(&ctx, dialects.BuiltinDialect.spec);
1129     try core_dialects.loadDialectSpec(&ctx, dialects.FuncDialect.spec);
1130 
1131     const loc = Location.getUnknown();
1132     const module = try dialects.BuiltinDialect.ModuleOp.create(&ctx, loc);
1133     const block = module.getBodyBlock();
1134     const declaration = try dialects.FuncDialect.FuncOp.createDeclaration(&ctx, loc, "external", &.{}, &.{});
1135     try testing.expect(SymbolTable.isDeclaration(declaration.op));
1136     try testing.expectEqual(SymbolTable.Visibility.private, SymbolTable.getSymbolVisibility(declaration.op));
1137     try testing.expectError(error.InvalidSymbolDeclaration, SymbolTable.setSymbolVisibility(declaration.op, .public));
1138     _ = declaration.op.removeAttr(SymbolTable.symbol_attr_names.sym_visibility);
1139     try testing.expectEqual(SymbolTable.Visibility.public, SymbolTable.getSymbolVisibility(declaration.op));
1140     try block.addOperation(declaration.op);
1141 
1142     try testing.expectError(
1143         error.InvalidSymbolDeclaration,
1144         @import("verify.zig").verifyOperation(module.op, @import("verify.zig").default_options),
1145     );
1146 }
1147 
1148 test "lookupNearestSymbolFrom stays within nearest symbol table" {
1149     const testing = std.testing;
1150     var arena = alloc_arena.Arena.init(std.testing.allocator);
1151     defer arena.deinit();
1152     const allocator = arena.allocator();
1153 
1154     const test_dialect = @import("../dialects/fixture/root.zig");
1155     const Location = @import("location.zig").Location;
1156 
1157     const Context = @import("context/root.zig").Context;
1158     var ctx = try Context.init(allocator, Context.Limits.testing);
1159     defer ctx.deinit(allocator);
1160     try ctx.allowUnregistered();
1161     try test_dialect.registerTestDialect(&ctx);
1162 
1163     const loc = Location.getUnknown();
1164     const outer = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
1165     const outer_block = outer.getBodyBlock();
1166     const outer_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "outer", &.{});
1167     try outer_block.addOperation(outer_func.op);
1168 
1169     const inner = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
1170     try outer_block.addOperation(inner.op);
1171     const inner_block = inner.getBodyBlock();
1172     const inner_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "inner", &.{});
1173     try inner_block.addOperation(inner_func.op);
1174 
1175     try testing.expect(SymbolTable.lookupNearestSymbolFrom(inner_func.op, "inner") == inner_func.op);
1176     try testing.expect(SymbolTable.lookupNearestSymbolFrom(inner_func.op, "outer") == null);
1177 }
1178 
1179 test "SymbolTable.Collection caches tables until invalidated" {
1180     const testing = std.testing;
1181     var arena = alloc_arena.Arena.init(std.testing.allocator);
1182     defer arena.deinit();
1183     const allocator = arena.allocator();
1184 
1185     const dialects = @import("../dialects/root.zig");
1186     const core_dialects = @import("root.zig").dialects;
1187     const Context = @import("context/root.zig").Context;
1188     const Location = @import("location.zig").Location;
1189 
1190     var ctx = try Context.init(allocator, Context.Limits.testing);
1191     defer ctx.deinit(allocator);
1192     try ctx.allowUnregistered();
1193     try core_dialects.loadDialectSpec(&ctx, dialects.BuiltinDialect.spec);
1194     try core_dialects.loadDialectSpec(&ctx, dialects.FuncDialect.spec);
1195 
1196     const loc = Location.getUnknown();
1197     const module = try dialects.BuiltinDialect.ModuleOp.create(&ctx, loc);
1198     const body = module.getBodyBlock();
1199     const target = try dialects.FuncDialect.FuncOp.createDeclaration(&ctx, loc, "target", &.{}, &.{});
1200     try body.addOperation(target.op);
1201     const caller = try dialects.FuncDialect.FuncOp.create(&ctx, loc, "caller", &.{}, &.{});
1202     try body.addOperation(caller.op);
1203 
1204     var collection = SymbolTable.Collection.init(testing.allocator);
1205     defer collection.deinit();
1206 
1207     try testing.expect(try collection.lookupSymbolIn(module.op, "target") == target.op);
1208     try testing.expect(try collection.lookupNearestSymbolFrom(caller.op, "target") == target.op);
1209     try testing.expect(try collection.lookupSymbolIn(module.op, "late") == null);
1210 
1211     const late = try dialects.FuncDialect.FuncOp.createDeclaration(&ctx, loc, "late", &.{}, &.{});
1212     try body.addOperation(late.op);
1213 
1214     try testing.expect(try collection.lookupSymbolIn(module.op, "late") == null);
1215     collection.invalidateSymbolTable(module.op);
1216     try testing.expect(try collection.lookupSymbolIn(module.op, "late") == late.op);
1217     try testing.expect(try collection.lookupNearestSymbolFrom(caller.op, "late") == late.op);
1218 }
1219 
1220 test "SymbolTable.Collection resolves nested symbol references" {
1221     const testing = std.testing;
1222     var arena = alloc_arena.Arena.init(std.testing.allocator);
1223     defer arena.deinit();
1224     const allocator = arena.allocator();
1225 
1226     const test_dialect = @import("../dialects/fixture/root.zig");
1227     const Context = @import("context/root.zig").Context;
1228     const Location = @import("location.zig").Location;
1229 
1230     var ctx = try Context.init(allocator, Context.Limits.testing);
1231     defer ctx.deinit(allocator);
1232     try ctx.allowUnregistered();
1233     try test_dialect.registerTestDialect(&ctx);
1234 
1235     try ctx.registerOperationInterfaceExternal(
1236         "test.module",
1237         interfaces.SymbolOpInterface.entry(&TestModuleSymbol.vtable),
1238     );
1239 
1240     const loc = Location.getUnknown();
1241     const outer = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
1242     const inner = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
1243     try inner.op.setAttr("sym_name", try test_dialect.TestDialect.getStringAttr(&ctx, "nested"));
1244     try outer.getBodyBlock().addOperation(inner.op);
1245 
1246     const leaf = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "leaf", &.{});
1247     try inner.getBodyBlock().addOperation(leaf.op);
1248 
1249     const nested_attr = try ctx.getSymbolRefAttr("nested", &.{"leaf"});
1250     const nested_ref = nested_attr.cast(Attribute.SymbolRefAttr).?;
1251     var collection = SymbolTable.Collection.init(testing.allocator);
1252     defer collection.deinit();
1253 
1254     try testing.expect(try collection.lookupSymbolRefIn(outer.op, nested_ref) == leaf.op);
1255     try SymbolTable.setSymbolVisibility(leaf.op, .private);
1256     try testing.expect(try collection.lookupSymbolRefIn(outer.op, nested_ref) == null);
1257     try SymbolTable.setSymbolVisibility(leaf.op, .nested);
1258     try testing.expect(try collection.lookupSymbolRefIn(outer.op, nested_ref) == leaf.op);
1259 }
1260 
1261 test "SymbolTable.UserMap replaces known flat symbol users" {
1262     const testing = std.testing;
1263     var arena = alloc_arena.Arena.init(std.testing.allocator);
1264     defer arena.deinit();
1265     const allocator = arena.allocator();
1266 
1267     const dialects = @import("../dialects/root.zig");
1268     const core_dialects = @import("root.zig").dialects;
1269     const Context = @import("context/root.zig").Context;
1270     const Location = @import("location.zig").Location;
1271 
1272     var ctx = try Context.init(allocator, Context.Limits.testing);
1273     defer ctx.deinit(allocator);
1274     try ctx.allowUnregistered();
1275     try core_dialects.loadDialectSpec(&ctx, dialects.BuiltinDialect.spec);
1276     try core_dialects.loadDialectSpec(&ctx, dialects.arith.spec);
1277     try core_dialects.loadDialectSpec(&ctx, dialects.FuncDialect.spec);
1278 
1279     const loc = Location.getUnknown();
1280     const i32_type = try dialects.ArithDialect.getI32Type(&ctx);
1281 
1282     const module = try dialects.BuiltinDialect.ModuleOp.create(&ctx, loc);
1283     const body = module.getBodyBlock();
1284     const target = try dialects.FuncDialect.FuncOp.createDeclaration(&ctx, loc, "target", &.{i32_type}, &.{i32_type});
1285     const existing = try dialects.FuncDialect.FuncOp.createDeclaration(&ctx, loc, "existing", &.{i32_type}, &.{i32_type});
1286     try body.addOperation(target.op);
1287     try body.addOperation(existing.op);
1288 
1289     var caller = try dialects.FuncDialect.FuncOp.create(&ctx, loc, "caller", &.{i32_type}, &.{i32_type});
1290     try body.addOperation(caller.op);
1291     const call = try dialects.FuncDialect.CallOp.create(&ctx, loc, "target", &.{caller.getArgument(0)}, &.{i32_type});
1292     try caller.getEntryBlock().addOperation(call.op);
1293 
1294     var collection = SymbolTable.Collection.init(testing.allocator);
1295     defer collection.deinit();
1296     var users = try SymbolUserMap.init(testing.allocator, &collection, module.op);
1297     defer users.deinit();
1298 
1299     try testing.expectEqual(@as(usize, 1), users.getUsers(target.op).len);
1300     try testing.expect(users.getUsers(target.op)[0] == call.op);
1301     try testing.expect(users.useEmpty(existing.op));
1302 
1303     const replaced = try users.replaceAllUsesWith(target.op, "existing");
1304 
1305     try testing.expectEqual(@as(usize, 1), replaced);
1306     try testing.expectEqualStrings("existing", call.getCallee().?);
1307     try testing.expect(users.useEmpty(target.op));
1308     try testing.expectEqual(@as(usize, 1), users.getUsers(existing.op).len);
1309     try testing.expect(users.getUsers(existing.op)[0] == call.op);
1310 }
1311 
1312 test "SymbolTable verifies discardable symbol user attributes" {
1313     const testing = std.testing;
1314     var arena = alloc_arena.Arena.init(std.testing.allocator);
1315     defer arena.deinit();
1316     const allocator = arena.allocator();
1317 
1318     const test_dialect = @import("../dialects/fixture/root.zig");
1319     const Context = @import("context/root.zig").Context;
1320     const Location = @import("location.zig").Location;
1321 
1322     var ctx = try Context.init(allocator, Context.Limits.testing);
1323     defer ctx.deinit(allocator);
1324     try ctx.allowUnregistered();
1325     try test_dialect.registerTestDialect(&ctx);
1326 
1327     _ = try ctx.registerAttributeType("test.symbol_user_attr", &.{
1328         interfaces.SymbolUserAttrInterface.entryFor(TestSymbolUserAttribute.verifySymbolUses),
1329     });
1330 
1331     const loc = Location.getUnknown();
1332     const module = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
1333     const body = module.getBodyBlock();
1334     const target = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "target", &.{});
1335     try body.addOperation(target.op);
1336     const user = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "user", &.{});
1337     try body.addOperation(user.op);
1338 
1339     try user.op.setDiscardableAttr("test.ref", try ctx.getDialectAttr("test.symbol_user_attr", "target"));
1340     try @import("verify.zig").verifyOperation(module.op, @import("verify.zig").default_options);
1341 
1342     try user.op.setDiscardableAttr("test.ref", try ctx.getDialectAttr("test.symbol_user_attr", "missing"));
1343     try testing.expectError(
1344         error.UnresolvedAttrSymbol,
1345         @import("verify.zig").verifyOperation(module.op, @import("verify.zig").default_options),
1346     );
1347 }
1348 
1349 test "SymbolTable.UserMap records and replaces nested symbol users" {
1350     const testing = std.testing;
1351     var arena = alloc_arena.Arena.init(std.testing.allocator);
1352     defer arena.deinit();
1353     const allocator = arena.allocator();
1354 
1355     const test_dialect = @import("../dialects/fixture/root.zig");
1356     const Context = @import("context/root.zig").Context;
1357     const Location = @import("location.zig").Location;
1358 
1359     var ctx = try Context.init(allocator, Context.Limits.testing);
1360     defer ctx.deinit(allocator);
1361     try ctx.allowUnregistered();
1362     try test_dialect.registerTestDialect(&ctx);
1363 
1364     try ctx.registerOperationInterfaceExternal(
1365         "test.module",
1366         interfaces.SymbolOpInterface.entry(&TestModuleSymbol.vtable),
1367     );
1368 
1369     const loc = Location.getUnknown();
1370     const outer = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
1371     const inner = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
1372     try inner.op.setAttr("sym_name", try test_dialect.TestDialect.getStringAttr(&ctx, "nested"));
1373     try outer.getBodyBlock().addOperation(inner.op);
1374 
1375     const leaf = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "leaf", &.{});
1376     const new_leaf = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "new_leaf", &.{});
1377     try inner.getBodyBlock().addOperation(leaf.op);
1378     try inner.getBodyBlock().addOperation(new_leaf.op);
1379 
1380     const user = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "user", &.{});
1381     try user.op.setAttr("nested_ref", try ctx.getSymbolRefAttr("nested", &.{"leaf"}));
1382     try outer.getBodyBlock().addOperation(user.op);
1383 
1384     var collection = SymbolTable.Collection.init(testing.allocator);
1385     defer collection.deinit();
1386     var users = try SymbolUserMap.init(testing.allocator, &collection, outer.op);
1387     defer users.deinit();
1388 
1389     try testing.expectEqual(@as(usize, 1), users.getUsers(inner.op).len);
1390     try testing.expect(users.getUsers(inner.op)[0] == user.op);
1391     try testing.expectEqual(@as(usize, 1), users.getUsers(leaf.op).len);
1392     try testing.expect(users.getUsers(leaf.op)[0] == user.op);
1393 
1394     const replaced = try users.replaceAllUsesWith(leaf.op, "new_leaf");
1395 
1396     try testing.expectEqual(@as(usize, 1), replaced);
1397     const updated = user.op.getAttrAs(Attribute.SymbolRefAttr, "nested_ref") orelse return error.TestExpectedAttribute;
1398     try testing.expectEqualStrings("nested", updated.getRootReference());
1399     try testing.expectEqual(@as(usize, 1), updated.getNestedReferences().len);
1400     try testing.expectEqualStrings("new_leaf", updated.getNestedReferences()[0]);
1401     try testing.expect(users.useEmpty(leaf.op));
1402     try testing.expectEqual(@as(usize, 1), users.getUsers(new_leaf.op).len);
1403     try testing.expect(users.getUsers(new_leaf.op)[0] == user.op);
1404 }
1405 
1406 test "SymbolTable resolves nested symbol reference attributes" {
1407     const testing = std.testing;
1408     var arena = alloc_arena.Arena.init(std.testing.allocator);
1409     defer arena.deinit();
1410     const allocator = arena.allocator();
1411 
1412     const test_dialect = @import("../dialects/fixture/root.zig");
1413     const Location = @import("location.zig").Location;
1414 
1415     const Context = @import("context/root.zig").Context;
1416     var ctx = try Context.init(allocator, Context.Limits.testing);
1417     defer ctx.deinit(allocator);
1418     try ctx.allowUnregistered();
1419     try test_dialect.registerTestDialect(&ctx);
1420 
1421     try ctx.registerOperationInterfaceExternal(
1422         "test.module",
1423         interfaces.SymbolOpInterface.entry(&TestModuleSymbol.vtable),
1424     );
1425 
1426     const loc = Location.getUnknown();
1427     const outer = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
1428     const outer_block = outer.getBodyBlock();
1429 
1430     const inner = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
1431     try inner.op.setAttr("sym_name", try test_dialect.TestDialect.getStringAttr(&ctx, "nested"));
1432     try outer_block.addOperation(inner.op);
1433 
1434     const inner_func = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "leaf", &.{});
1435     try inner.getBodyBlock().addOperation(inner_func.op);
1436 
1437     const nested_attr = try ctx.getSymbolRefAttr("nested", &.{"leaf"});
1438     const nested_ref = nested_attr.cast(Attribute.SymbolRefAttr).?;
1439     try testing.expect(SymbolTable.lookupSymbolRefIn(outer.op, nested_ref) == inner_func.op);
1440 
1441     const flat_attr = try ctx.getFlatSymbolRefAttr("leaf");
1442     const flat_ref = flat_attr.cast(Attribute.SymbolRefAttr).?;
1443     try testing.expect(SymbolTable.lookupNearestSymbolRefFrom(inner_func.op, flat_ref) == inner_func.op);
1444 }
1445 
1446 test "SymbolTable collects symbol uses without crossing nested symbol tables" {
1447     const testing = std.testing;
1448     var arena = alloc_arena.Arena.init(std.testing.allocator);
1449     defer arena.deinit();
1450     const allocator = arena.allocator();
1451 
1452     const dialects = @import("../dialects/root.zig");
1453     const core_dialects = @import("root.zig").dialects;
1454     const Context = @import("context/root.zig").Context;
1455     const Location = @import("location.zig").Location;
1456 
1457     var ctx = try Context.init(allocator, Context.Limits.testing);
1458     defer ctx.deinit(allocator);
1459     try ctx.allowUnregistered();
1460     try core_dialects.loadDialectSpec(&ctx, dialects.BuiltinDialect.spec);
1461     try core_dialects.loadDialectSpec(&ctx, dialects.arith.spec);
1462     try core_dialects.loadDialectSpec(&ctx, dialects.FuncDialect.spec);
1463 
1464     const loc = Location.getUnknown();
1465     const i32_type = try dialects.ArithDialect.getI32Type(&ctx);
1466 
1467     const outer = try dialects.BuiltinDialect.ModuleOp.create(&ctx, loc);
1468     const outer_block = outer.getBodyBlock();
1469     const target = try dialects.FuncDialect.FuncOp.createDeclaration(&ctx, loc, "target", &.{i32_type}, &.{i32_type});
1470     try outer_block.addOperation(target.op);
1471 
1472     var caller = try dialects.FuncDialect.FuncOp.create(&ctx, loc, "caller", &.{i32_type}, &.{i32_type});
1473     try outer_block.addOperation(caller.op);
1474     const call = try dialects.FuncDialect.CallOp.create(&ctx, loc, "target", &.{caller.getArgument(0)}, &.{i32_type});
1475     try caller.getEntryBlock().addOperation(call.op);
1476 
1477     const inner = try dialects.BuiltinDialect.ModuleOp.create(&ctx, loc);
1478     try outer_block.addOperation(inner.op);
1479     var inner_caller = try dialects.FuncDialect.FuncOp.create(&ctx, loc, "inner_caller", &.{i32_type}, &.{i32_type});
1480     try inner.getBodyBlock().addOperation(inner_caller.op);
1481     const inner_call = try dialects.FuncDialect.CallOp.create(&ctx, loc, "target", &.{inner_caller.getArgument(0)}, &.{i32_type});
1482     try inner_caller.getEntryBlock().addOperation(inner_call.op);
1483 
1484     var uses = try SymbolTable.collectSymbolUsesInSymbolTable(testing.allocator, outer.op);
1485     defer uses.deinit(testing.allocator);
1486 
1487     try testing.expectEqual(@as(usize, 1), uses.items().len);
1488     try testing.expect(uses.items()[0].user == call.op);
1489     try testing.expectEqualStrings("callee", uses.items()[0].attr_name);
1490     try testing.expectEqualStrings("target", uses.items()[0].symbol_ref.getLeafReference());
1491 
1492     const target_ref_attr = try ctx.getFlatSymbolRefAttr("target");
1493     const target_ref = target_ref_attr.cast(Attribute.SymbolRefAttr).?;
1494     try testing.expect(!try SymbolTable.symbolKnownUseEmptyInSymbolTable(testing.allocator, outer.op, target_ref));
1495 }
1496 
1497 test "SymbolTable replaces symbol uses without crossing nested symbol tables" {
1498     const testing = std.testing;
1499     var arena = alloc_arena.Arena.init(std.testing.allocator);
1500     defer arena.deinit();
1501     const allocator = arena.allocator();
1502 
1503     const dialects = @import("../dialects/root.zig");
1504     const core_dialects = @import("root.zig").dialects;
1505     const Context = @import("context/root.zig").Context;
1506     const Location = @import("location.zig").Location;
1507 
1508     var ctx = try Context.init(allocator, Context.Limits.testing);
1509     defer ctx.deinit(allocator);
1510     try ctx.allowUnregistered();
1511     try core_dialects.loadDialectSpec(&ctx, dialects.BuiltinDialect.spec);
1512     try core_dialects.loadDialectSpec(&ctx, dialects.arith.spec);
1513     try core_dialects.loadDialectSpec(&ctx, dialects.FuncDialect.spec);
1514 
1515     const loc = Location.getUnknown();
1516     const i32_type = try dialects.ArithDialect.getI32Type(&ctx);
1517 
1518     const outer = try dialects.BuiltinDialect.ModuleOp.create(&ctx, loc);
1519     const outer_block = outer.getBodyBlock();
1520 
1521     var caller = try dialects.FuncDialect.FuncOp.create(&ctx, loc, "caller", &.{i32_type}, &.{i32_type});
1522     try outer_block.addOperation(caller.op);
1523     var call = try dialects.FuncDialect.CallOp.create(&ctx, loc, "target", &.{caller.getArgument(0)}, &.{i32_type});
1524     try caller.getEntryBlock().addOperation(call.op);
1525 
1526     const inner = try dialects.BuiltinDialect.ModuleOp.create(&ctx, loc);
1527     try outer_block.addOperation(inner.op);
1528     var inner_caller = try dialects.FuncDialect.FuncOp.create(&ctx, loc, "inner_caller", &.{i32_type}, &.{i32_type});
1529     try inner.getBodyBlock().addOperation(inner_caller.op);
1530     var inner_call = try dialects.FuncDialect.CallOp.create(&ctx, loc, "target", &.{inner_caller.getArgument(0)}, &.{i32_type});
1531     try inner_caller.getEntryBlock().addOperation(inner_call.op);
1532 
1533     const target_ref_attr = try ctx.getFlatSymbolRefAttr("target");
1534     const target_ref = target_ref_attr.cast(Attribute.SymbolRefAttr).?;
1535 
1536     const replaced = try SymbolTable.replaceAllSymbolUsesInSymbolTable(testing.allocator, outer.op, target_ref, "renamed");
1537 
1538     try testing.expectEqual(@as(usize, 1), replaced);
1539     try testing.expectEqualStrings("renamed", call.getCallee().?);
1540     try testing.expectEqualStrings("target", inner_call.getCallee().?);
1541     try testing.expect(try SymbolTable.symbolKnownUseEmptyInSymbolTable(testing.allocator, outer.op, target_ref));
1542 }
1543 
1544 test "SymbolTable replaces nested symbol reference prefixes" {
1545     const testing = std.testing;
1546     var arena = alloc_arena.Arena.init(std.testing.allocator);
1547     defer arena.deinit();
1548     const allocator = arena.allocator();
1549 
1550     const test_dialect = @import("../dialects/fixture/root.zig");
1551     const Context = @import("context/root.zig").Context;
1552     const Location = @import("location.zig").Location;
1553 
1554     var ctx = try Context.init(allocator, Context.Limits.testing);
1555     defer ctx.deinit(allocator);
1556     try ctx.allowUnregistered();
1557     try test_dialect.registerTestDialect(&ctx);
1558 
1559     const loc = Location.getUnknown();
1560     const module = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
1561     const body = module.getBodyBlock();
1562     const user = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "user", &.{});
1563     try user.op.setAttr("nested_ref", try ctx.getSymbolRefAttr("module", &.{ "old", "leaf" }));
1564     try body.addOperation(user.op);
1565 
1566     const old_attr = try ctx.getSymbolRefAttr("module", &.{"old"});
1567     const old_ref = old_attr.cast(Attribute.SymbolRefAttr).?;
1568     const replaced = try SymbolTable.replaceAllSymbolUsesInSymbolTable(testing.allocator, module.op, old_ref, "new");
1569 
1570     try testing.expectEqual(@as(usize, 1), replaced);
1571     const updated = user.op.getAttrAs(Attribute.SymbolRefAttr, "nested_ref") orelse return error.TestExpectedAttribute;
1572     try testing.expectEqualStrings("module", updated.getRootReference());
1573     try testing.expectEqual(@as(usize, 2), updated.getNestedReferences().len);
1574     try testing.expectEqualStrings("new", updated.getNestedReferences()[0]);
1575     try testing.expectEqualStrings("leaf", updated.getNestedReferences()[1]);
1576 }
1577 
1578 test "SymbolTable rejects replacement of read-only inherent symbol references" {
1579     const testing = std.testing;
1580     var arena = alloc_arena.Arena.init(testing.allocator);
1581     defer arena.deinit();
1582     const allocator = arena.allocator();
1583 
1584     const dialects = @import("../dialects/root.zig");
1585     const core = @import("root.zig");
1586     const core_dialects = core.dialects;
1587     const Context = core.Context;
1588     const Location = core.Location;
1589 
1590     var ctx = try Context.init(allocator, Context.Limits.testing);
1591     defer ctx.deinit(allocator);
1592     try ctx.allowUnregistered();
1593     try core_dialects.loadDialectSpec(&ctx, dialects.BuiltinDialect.spec);
1594     _ = try ctx.registerOperation("test.read_only_symbol_user", .{});
1595     try ctx.registerOperationInherentAttributeName("test.read_only_symbol_user", "callee");
1596     try ctx.registerOperationPropertiesModel(
1597         "test.read_only_symbol_user",
1598         ReadOnlySymbolRefProperties.model,
1599     );
1600 
1601     const module = try dialects.BuiltinDialect.ModuleOp.create(&ctx, Location.getUnknown());
1602     const target_attr = try ctx.getFlatSymbolRefAttr("target");
1603     var state = Operation.State.init("test.read_only_symbol_user", Location.getUnknown());
1604     try state.setPropertiesAttr(target_attr);
1605     const user = try ctx.createOperation(state);
1606     try module.getBodyBlock().addOperation(user);
1607 
1608     const target_ref = target_attr.cast(Attribute.SymbolRefAttr).?;
1609     try testing.expectError(
1610         error.ReadOnlyInherentAttribute,
1611         SymbolTable.replaceAllSymbolUsesInSymbolTable(
1612             testing.allocator,
1613             module.op,
1614             target_ref,
1615             "renamed",
1616         ),
1617     );
1618     const preserved = user.getAttrAs(Attribute.SymbolRefAttr, "callee") orelse
1619         return error.TestExpectedAttribute;
1620     try testing.expectEqualStrings("target", preserved.getRootReference());
1621     try testing.expect(user.raw_dictionary_attrs.get("callee") == null);
1622 }
1623 
1624 test "SymbolTable collects and replaces array symbol reference attributes" {
1625     const testing = std.testing;
1626     var arena = alloc_arena.Arena.init(std.testing.allocator);
1627     defer arena.deinit();
1628     const allocator = arena.allocator();
1629 
1630     const test_dialect = @import("../dialects/fixture/root.zig");
1631     const Context = @import("context/root.zig").Context;
1632     const Location = @import("location.zig").Location;
1633 
1634     var ctx = try Context.init(allocator, Context.Limits.testing);
1635     defer ctx.deinit(allocator);
1636     try ctx.allowUnregistered();
1637     try test_dialect.registerTestDialect(&ctx);
1638 
1639     const loc = Location.getUnknown();
1640     const module = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
1641     const body = module.getBodyBlock();
1642     const target = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "target", &.{});
1643     try body.addOperation(target.op);
1644     const user = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "user", &.{});
1645     try body.addOperation(user.op);
1646 
1647     const keep_attr = try ctx.getStringAttr("keep");
1648     const target_ref_attr = try ctx.getFlatSymbolRefAttr("target");
1649     const target_ref = target_ref_attr.cast(Attribute.SymbolRefAttr).?;
1650     const nested_array_attr = try ctx.getArrayAttr(&.{ target_ref_attr, keep_attr });
1651     const array_attr = try ctx.getArrayAttr(&.{ keep_attr, nested_array_attr });
1652     try user.op.setAttr("refs", array_attr);
1653 
1654     var uses = try SymbolTable.collectSymbolUsesInSymbolTable(testing.allocator, module.op);
1655     defer uses.deinit(testing.allocator);
1656     try testing.expectEqual(@as(usize, 1), uses.items().len);
1657     try testing.expect(uses.items()[0].user == user.op);
1658     try testing.expectEqualStrings("refs", uses.items()[0].attr_name);
1659     try testing.expectEqualStrings("target", uses.items()[0].symbol_ref.getRootReference());
1660 
1661     const replaced = try SymbolTable.replaceAllSymbolUsesInSymbolTable(testing.allocator, module.op, target_ref, "renamed");
1662 
1663     try testing.expectEqual(@as(usize, 1), replaced);
1664     const updated_array = user.op.getAttrAs(Attribute.ArrayAttr, "refs") orelse return error.TestExpectedAttribute;
1665     try testing.expectEqual(@as(usize, 2), updated_array.values.len);
1666     try testing.expect(updated_array.values[0].eql(keep_attr));
1667 
1668     const updated_nested_array = updated_array.values[1].cast(Attribute.ArrayAttr) orelse return error.TestExpectedAttribute;
1669     try testing.expectEqual(@as(usize, 2), updated_nested_array.values.len);
1670     const updated_ref = updated_nested_array.values[0].cast(Attribute.SymbolRefAttr) orelse return error.TestExpectedAttribute;
1671     try testing.expectEqualStrings("renamed", updated_ref.getRootReference());
1672     try testing.expect(updated_nested_array.values[1].eql(keep_attr));
1673     try testing.expect(try SymbolTable.symbolKnownUseEmptyInSymbolTable(testing.allocator, module.op, target_ref));
1674 }
1675 
1676 test "SymbolTable renames symbols through array symbol reference attributes" {
1677     const testing = std.testing;
1678     var arena = alloc_arena.Arena.init(std.testing.allocator);
1679     defer arena.deinit();
1680     const allocator = arena.allocator();
1681 
1682     const test_dialect = @import("../dialects/fixture/root.zig");
1683     const Context = @import("context/root.zig").Context;
1684     const Location = @import("location.zig").Location;
1685 
1686     var ctx = try Context.init(allocator, Context.Limits.testing);
1687     defer ctx.deinit(allocator);
1688     try ctx.allowUnregistered();
1689     try test_dialect.registerTestDialect(&ctx);
1690 
1691     const loc = Location.getUnknown();
1692     const module = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
1693     const body = module.getBodyBlock();
1694     const target = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "target", &.{});
1695     try body.addOperation(target.op);
1696     const user = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "user", &.{});
1697     try body.addOperation(user.op);
1698 
1699     const target_ref_attr = try ctx.getFlatSymbolRefAttr("target");
1700     const array_attr = try ctx.getArrayAttr(&.{target_ref_attr});
1701     try user.op.setAttr("refs", array_attr);
1702 
1703     {
1704         var collection = SymbolTable.Collection.init(testing.allocator);
1705         defer collection.deinit();
1706         var users = try SymbolUserMap.init(testing.allocator, &collection, module.op);
1707         defer users.deinit();
1708         try testing.expectEqual(@as(usize, 1), users.getUsers(target.op).len);
1709         try testing.expect(users.getUsers(target.op)[0] == user.op);
1710     }
1711 
1712     const renamed = try SymbolTable.renameSymbolInSymbolTable(testing.allocator, module.op, target.op, "renamed");
1713 
1714     try testing.expectEqual(@as(usize, 1), renamed);
1715     try testing.expectEqualStrings("renamed", target.getName().?);
1716     const updated_array = user.op.getAttrAs(Attribute.ArrayAttr, "refs") orelse return error.TestExpectedAttribute;
1717     try testing.expectEqual(@as(usize, 1), updated_array.values.len);
1718     const updated_ref = updated_array.values[0].cast(Attribute.SymbolRefAttr) orelse return error.TestExpectedAttribute;
1719     try testing.expectEqualStrings("renamed", updated_ref.getRootReference());
1720     try testing.expect(SymbolTable.lookupSymbolIn(module.op, "target") == null);
1721     try testing.expect(SymbolTable.lookupSymbolIn(module.op, "renamed") == target.op);
1722 }
1723 
1724 test "SymbolTable renames symbols and rewrites scoped uses" {
1725     const testing = std.testing;
1726     var arena = alloc_arena.Arena.init(std.testing.allocator);
1727     defer arena.deinit();
1728     const allocator = arena.allocator();
1729 
1730     const dialects = @import("../dialects/root.zig");
1731     const core_dialects = @import("root.zig").dialects;
1732     const Context = @import("context/root.zig").Context;
1733     const Location = @import("location.zig").Location;
1734 
1735     var ctx = try Context.init(allocator, Context.Limits.testing);
1736     defer ctx.deinit(allocator);
1737     try ctx.allowUnregistered();
1738     try core_dialects.loadDialectSpec(&ctx, dialects.BuiltinDialect.spec);
1739     try core_dialects.loadDialectSpec(&ctx, dialects.arith.spec);
1740     try core_dialects.loadDialectSpec(&ctx, dialects.FuncDialect.spec);
1741 
1742     const loc = Location.getUnknown();
1743     const i32_type = try dialects.ArithDialect.getI32Type(&ctx);
1744 
1745     const module = try dialects.BuiltinDialect.ModuleOp.create(&ctx, loc);
1746     const body = module.getBodyBlock();
1747     const target = try dialects.FuncDialect.FuncOp.createDeclaration(&ctx, loc, "target", &.{i32_type}, &.{i32_type});
1748     try body.addOperation(target.op);
1749 
1750     var caller = try dialects.FuncDialect.FuncOp.create(&ctx, loc, "caller", &.{i32_type}, &.{i32_type});
1751     try body.addOperation(caller.op);
1752     const call = try dialects.FuncDialect.CallOp.create(&ctx, loc, "target", &.{caller.getArgument(0)}, &.{i32_type});
1753     try caller.getEntryBlock().addOperation(call.op);
1754 
1755     const renamed = try SymbolTable.renameSymbolInSymbolTable(testing.allocator, module.op, target.op, "renamed");
1756 
1757     try testing.expectEqual(@as(usize, 1), renamed);
1758     try testing.expectEqualStrings("renamed", target.getName().?);
1759     try testing.expectEqualStrings("renamed", SymbolTable.getSymbolName(target.op).?);
1760     try testing.expectEqualStrings("renamed", call.getCallee().?);
1761     try testing.expect(SymbolTable.lookupSymbolIn(module.op, "target") == null);
1762     try testing.expect(SymbolTable.lookupSymbolIn(module.op, "renamed") == target.op);
1763 }
1764 
1765 test "SymbolTable rename rejects collisions before rewriting uses" {
1766     const testing = std.testing;
1767     var arena = alloc_arena.Arena.init(std.testing.allocator);
1768     defer arena.deinit();
1769     const allocator = arena.allocator();
1770 
1771     const dialects = @import("../dialects/root.zig");
1772     const core_dialects = @import("root.zig").dialects;
1773     const Context = @import("context/root.zig").Context;
1774     const Location = @import("location.zig").Location;
1775 
1776     var ctx = try Context.init(allocator, Context.Limits.testing);
1777     defer ctx.deinit(allocator);
1778     try ctx.allowUnregistered();
1779     try core_dialects.loadDialectSpec(&ctx, dialects.BuiltinDialect.spec);
1780     try core_dialects.loadDialectSpec(&ctx, dialects.arith.spec);
1781     try core_dialects.loadDialectSpec(&ctx, dialects.FuncDialect.spec);
1782 
1783     const loc = Location.getUnknown();
1784     const i32_type = try dialects.ArithDialect.getI32Type(&ctx);
1785 
1786     const module = try dialects.BuiltinDialect.ModuleOp.create(&ctx, loc);
1787     const body = module.getBodyBlock();
1788     const target = try dialects.FuncDialect.FuncOp.createDeclaration(&ctx, loc, "target", &.{i32_type}, &.{i32_type});
1789     const existing = try dialects.FuncDialect.FuncOp.createDeclaration(&ctx, loc, "existing", &.{i32_type}, &.{i32_type});
1790     try body.addOperation(target.op);
1791     try body.addOperation(existing.op);
1792 
1793     var caller = try dialects.FuncDialect.FuncOp.create(&ctx, loc, "caller", &.{i32_type}, &.{i32_type});
1794     try body.addOperation(caller.op);
1795     const call = try dialects.FuncDialect.CallOp.create(&ctx, loc, "target", &.{caller.getArgument(0)}, &.{i32_type});
1796     try caller.getEntryBlock().addOperation(call.op);
1797 
1798     try testing.expectError(
1799         error.DuplicateSymbol,
1800         SymbolTable.renameSymbolInSymbolTable(testing.allocator, module.op, target.op, "existing"),
1801     );
1802     try testing.expectEqualStrings("target", target.getName().?);
1803     try testing.expectEqualStrings("target", call.getCallee().?);
1804 }
1805 
1806 test "SymbolTable visibility hides private nested symbol references" {
1807     const testing = std.testing;
1808     var arena = alloc_arena.Arena.init(std.testing.allocator);
1809     defer arena.deinit();
1810     const allocator = arena.allocator();
1811 
1812     const test_dialect = @import("../dialects/fixture/root.zig");
1813     const Context = @import("context/root.zig").Context;
1814     const Location = @import("location.zig").Location;
1815 
1816     var ctx = try Context.init(allocator, Context.Limits.testing);
1817     defer ctx.deinit(allocator);
1818     try ctx.allowUnregistered();
1819     try test_dialect.registerTestDialect(&ctx);
1820 
1821     try ctx.registerOperationInterfaceExternal(
1822         "test.module",
1823         interfaces.SymbolOpInterface.entry(&TestModuleSymbol.vtable),
1824     );
1825 
1826     const loc = Location.getUnknown();
1827     const outer = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
1828     const inner = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
1829     try inner.op.setAttr("sym_name", try test_dialect.TestDialect.getStringAttr(&ctx, "nested"));
1830     try outer.getBodyBlock().addOperation(inner.op);
1831 
1832     const leaf = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "leaf", &.{});
1833     try inner.getBodyBlock().addOperation(leaf.op);
1834 
1835     const nested_attr = try ctx.getSymbolRefAttr("nested", &.{"leaf"});
1836     const nested_ref = nested_attr.cast(Attribute.SymbolRefAttr).?;
1837     try testing.expect(SymbolTable.lookupSymbolRefIn(outer.op, nested_ref) == leaf.op);
1838     try testing.expectEqual(SymbolTable.Visibility.public, SymbolTable.getSymbolVisibility(leaf.op));
1839 
1840     try SymbolTable.setSymbolVisibility(leaf.op, .private);
1841     try testing.expectEqual(SymbolTable.Visibility.private, SymbolTable.getSymbolVisibility(leaf.op));
1842     try testing.expect(SymbolTable.lookupSymbolRefIn(outer.op, nested_ref) == null);
1843     try testing.expect(leaf.op.getDiscardableAttr(SymbolTable.symbol_attr_names.sym_visibility) == null);
1844 
1845     try SymbolTable.setSymbolVisibility(leaf.op, .nested);
1846     try testing.expectEqual(SymbolTable.Visibility.nested, SymbolTable.getSymbolVisibility(leaf.op));
1847     try testing.expect(SymbolTable.lookupSymbolRefIn(outer.op, nested_ref) == leaf.op);
1848 
1849     try SymbolTable.setSymbolVisibility(leaf.op, .public);
1850     try testing.expectEqual(SymbolTable.Visibility.public, SymbolTable.getSymbolVisibility(leaf.op));
1851     try testing.expect(leaf.op.getAttr(SymbolTable.symbol_attr_names.sym_visibility) == null);
1852 }