lib/choir/src/core/attribute.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const interfaces = @import("interfaces/root.zig");
  3 const Type = @import("type.zig").Type;
  4 
  5 pub const Attribute = struct {
  6     attr_id: AttrID,
  7 
  8     impl: *const anyopaque,
  9 
 10     abstract: *const interfaces.AbstractAttribute,
 11 
 12     pub const AttrID = enum(u32) {
 13         invalid = 0,
 14         _,
 15     };
 16 
 17     pub const first_dynamic_attr_id: u32 = @backingInt(AttrID.invalid) + 1;
 18 
 19     pub const DialectAttr = struct {
 20         payload: []const u8,
 21         context: *const anyopaque,
 22     };
 23 
 24     pub const IntegerAttr = struct {
 25         value: i64,
 26         width: u8,
 27         is_signed: bool,
 28         context: *const anyopaque,
 29 
 30         pub fn getValue(self: *const IntegerAttr) i64 {
 31             return self.value;
 32         }
 33 
 34         pub fn getUnsignedValue(self: *const IntegerAttr) u64 {
 35             return @bitCast(self.value);
 36         }
 37     };
 38 
 39     pub const FloatAttr = struct {
 40         value: f64,
 41         width: u8,
 42         context: *const anyopaque,
 43 
 44         pub fn getValue(self: *const FloatAttr) f64 {
 45             return self.value;
 46         }
 47 
 48         pub fn getF32Value(self: *const FloatAttr) f32 {
 49             return @floatCast(self.value);
 50         }
 51     };
 52 
 53     pub const BoolAttr = struct {
 54         value: bool,
 55         context: *const anyopaque,
 56 
 57         pub fn getValue(self: *const BoolAttr) bool {
 58             return self.value;
 59         }
 60     };
 61 
 62     pub const StringAttr = struct {
 63         value: []const u8,
 64         context: *const anyopaque,
 65 
 66         pub fn getValue(self: *const StringAttr) []const u8 {
 67             return self.value;
 68         }
 69     };
 70 
 71     pub const SymbolRefAttr = struct {
 72         root_reference: []const u8,
 73         nested_references: []const []const u8,
 74         context: *const anyopaque,
 75 
 76         pub fn getRootReference(self: *const SymbolRefAttr) []const u8 {
 77             return self.root_reference;
 78         }
 79 
 80         pub fn getNestedReferences(self: *const SymbolRefAttr) []const []const u8 {
 81             return self.nested_references;
 82         }
 83 
 84         pub fn getLeafReference(self: *const SymbolRefAttr) []const u8 {
 85             if (self.nested_references.len == 0) return self.root_reference;
 86             return self.nested_references[self.nested_references.len - 1];
 87         }
 88 
 89         pub fn isFlat(self: *const SymbolRefAttr) bool {
 90             return self.nested_references.len == 0;
 91         }
 92     };
 93 
 94     pub const StringListAttr = struct {
 95         values: []const []const u8,
 96         context: *const anyopaque,
 97 
 98         pub fn getValues(self: *const StringListAttr) []const []const u8 {
 99             return self.values;
100         }
101     };
102 
103     pub const TypeListAttr = struct {
104         values: []const Type,
105         context: *const anyopaque,
106 
107         pub fn getValues(self: *const TypeListAttr) []const Type {
108             return self.values;
109         }
110     };
111 
112     pub const ArrayAttr = struct {
113         values: []const Attribute,
114         context: *const anyopaque,
115 
116         pub fn getValues(self: *const ArrayAttr) []const Attribute {
117             return self.values;
118         }
119     };
120 
121     pub fn attrIdFromInt(id: u32) AttrID {
122         return @fromBackingInt(@intCast(id));
123     }
124 
125     pub fn isa(self: Attribute, comptime attr_id: AttrID) bool {
126         return self.attr_id == attr_id;
127     }
128 
129     pub fn eql(self: Attribute, other: Attribute) bool {
130         if (self.attr_id != other.attr_id) return false;
131         if (self.impl == other.impl) return true;
132         if (self.getInterface(interfaces.AttributeEqualInterface)) |vtable| {
133             return vtable.eql(self.impl, other.impl);
134         }
135         return false;
136     }
137 
138     pub fn getInterface(self: Attribute, comptime IFace: type) ?*const IFace.VTable {
139         if (self.abstract.getInterface(IFace.id)) |iface| {
140             return @ptrCast(@alignCast(iface));
141         }
142         return null;
143     }
144 
145     pub fn InterfaceHandle(comptime IFace: type) type {
146         return struct {
147             attr: Attribute,
148             vtable: *const IFace.VTable,
149 
150             fn returnType(comptime fn_ptr_type: type) type {
151                 const ptr_info = @typeInfo(fn_ptr_type);
152                 const fn_type = switch (ptr_info) {
153                     .pointer => |p| p.child,
154                     else => @compileError("expected interface vtable field to be a function pointer"),
155                 };
156                 const fn_info = switch (@typeInfo(fn_type)) {
157                     .@"fn" => |f| f,
158                     else => @compileError("expected interface vtable field to be a function pointer"),
159                 };
160                 return fn_info.return_type orelse @compileError("generic interface vtable methods are not supported");
161             }
162 
163             fn methodFnPtrType(comptime method: std.meta.FieldEnum(IFace.VTable)) type {
164                 const dummy: IFace.VTable = undefined;
165                 return @TypeOf(@field(dummy, @tagName(method)));
166             }
167 
168             pub inline fn call(
169                 self: @This(),
170                 comptime method: std.meta.FieldEnum(IFace.VTable),
171                 args: anytype,
172             ) returnType(methodFnPtrType(method)) {
173                 const fn_ptr = @field(self.vtable, @tagName(method));
174                 return @call(.auto, fn_ptr, .{self.attr.impl} ++ args);
175             }
176         };
177     }
178 
179     pub fn interface(self: Attribute, comptime IFace: type) ?InterfaceHandle(IFace) {
180         const vtable = self.getInterface(IFace) orelse return null;
181         return .{ .attr = self, .vtable = vtable };
182     }
183 
184     pub fn getAbstractAttribute(self: Attribute) *const interfaces.AbstractAttribute {
185         return self.abstract;
186     }
187 
188     pub fn hasStorageType(self: Attribute, comptime T: type) bool {
189         return storageTypeMatchesName(T, self.abstract.name);
190     }
191 
192     pub fn cast(self: Attribute, comptime T: type) ?*const T {
193         if (!self.hasStorageType(T)) return null;
194         return @ptrCast(@alignCast(self.impl));
195     }
196 
197     pub fn format(self: Attribute, writer: *std.Io.Writer) std.Io.Writer.Error!void {
198         if (self.getInterface(interfaces.AttributePrintInterface)) |vtable| {
199             return vtable.print(self.impl, writer);
200         }
201 
202         if (self.abstract.name.len > 0) {
203             try writer.print("#attr<{s}>", .{self.abstract.name});
204         } else {
205             try writer.print("#attr<{d}>", .{@backingInt(self.attr_id)});
206         }
207     }
208 };
209 
210 pub const NamedAttribute = struct {
211     name: []const u8,
212     value: Attribute,
213 
214     pub fn format(self: NamedAttribute, writer: *std.Io.Writer) std.Io.Writer.Error!void {
215         try writer.print("{s} = {f}", .{ self.name, self.value });
216     }
217 };
218 
219 pub const NamedAttributeList = struct {
220     const small_capacity = 4;
221 
222     len: usize = 0,
223     owned_items: []NamedAttribute = &.{},
224     small_items: [small_capacity]NamedAttribute = undefined,
225 
226     pub const Lookup = struct {
227         found: bool,
228         index: usize,
229     };
230 
231     pub fn items(self: *const NamedAttributeList) []const NamedAttribute {
232         if (self.owned_items.len != 0) return self.owned_items[0..self.len];
233         return self.small_items[0..self.len];
234     }
235 
236     pub fn capacity(self: *const NamedAttributeList) usize {
237         if (self.owned_items.len != 0) return self.owned_items.len;
238         return small_capacity;
239     }
240 
241     fn storage(self: *NamedAttributeList) []NamedAttribute {
242         if (self.owned_items.len != 0) return self.owned_items;
243         return self.small_items[0..];
244     }
245 
246     pub fn deinit(self: *NamedAttributeList, allocator: std.mem.Allocator) void {
247         if (self.owned_items.len != 0) allocator.free(self.owned_items);
248         self.len = 0;
249         self.owned_items = &.{};
250     }
251 
252     pub fn clearRetainingCapacity(self: *NamedAttributeList) void {
253         self.len = 0;
254     }
255 
256     pub fn ensureTotalCapacity(
257         self: *NamedAttributeList,
258         allocator: std.mem.Allocator,
259         target_capacity: usize,
260     ) std.mem.Allocator.Error!void {
261         if (target_capacity <= self.capacity()) return;
262         if (self.owned_items.len != 0) {
263             self.owned_items = try allocator.realloc(self.owned_items, target_capacity);
264             return;
265         }
266         const allocated_items = try allocator.alloc(NamedAttribute, target_capacity);
267         @memcpy(allocated_items[0..self.len], self.small_items[0..self.len]);
268         self.owned_items = allocated_items;
269     }
270 
271     pub fn get(self: *const NamedAttributeList, name: []const u8) ?Attribute {
272         const result = self.findIndexOrInsertPos(name);
273         if (!result.found) return null;
274         return self.items()[result.index].value;
275     }
276 
277     pub fn getNamed(self: *const NamedAttributeList, name: []const u8) ?NamedAttribute {
278         const result = self.findIndexOrInsertPos(name);
279         if (!result.found) return null;
280         return self.items()[result.index];
281     }
282 
283     pub fn set(
284         self: *NamedAttributeList,
285         allocator: std.mem.Allocator,
286         attr_name: []const u8,
287         value: Attribute,
288     ) !?Attribute {
289         const result = self.findIndexOrInsertPos(attr_name);
290         if (result.found) {
291             const storage_items = self.storage();
292             const previous = storage_items[result.index].value;
293             storage_items[result.index].value = value;
294             return previous;
295         }
296         const required_capacity = std.math.add(usize, self.len, 1) catch
297             return error.OutOfMemory;
298         if (required_capacity > self.capacity()) {
299             try self.ensureTotalCapacity(
300                 allocator,
301                 @max(required_capacity, self.capacity() *| 2),
302             );
303         }
304         const storage_items = self.storage();
305         std.mem.copyBackwards(NamedAttribute, storage_items[result.index + 1 .. self.len + 1], storage_items[result.index..self.len]);
306         storage_items[result.index] = .{ .name = attr_name, .value = value };
307         self.len += 1;
308         return null;
309     }
310 
311     pub fn erase(self: *NamedAttributeList, attr_name: []const u8) ?Attribute {
312         const result = self.findIndexOrInsertPos(attr_name);
313         if (!result.found) return null;
314         const storage_items = self.storage();
315         const previous = storage_items[result.index].value;
316         std.mem.copyForwards(NamedAttribute, storage_items[result.index .. self.len - 1], storage_items[result.index + 1 .. self.len]);
317         self.len -= 1;
318         return previous;
319     }
320 
321     pub fn findIndexOrInsertPos(self: *const NamedAttributeList, name: []const u8) Lookup {
322         var left: usize = 0;
323         const current_items = self.items();
324         var right: usize = current_items.len;
325         while (left < right) {
326             const mid = left + (right - left) / 2;
327             const cmp = std.mem.order(u8, current_items[mid].name, name);
328             switch (cmp) {
329                 .eq => return .{ .found = true, .index = mid },
330                 .lt => left = mid + 1,
331                 .gt => right = mid,
332             }
333         }
334         return .{ .found = false, .index = left };
335     }
336 };
337 
338 test "NamedAttributeList keeps dictionary semantics" {
339     const testing = std.testing;
340     const TestAttr = struct {
341         const abstract = interfaces.AbstractAttribute{
342             .attr_id = 1,
343             .name = "test.attr",
344             .interfaces = &.{},
345         };
346         const alpha: u8 = 1;
347         const beta: u8 = 2;
348         const gamma: u8 = 3;
349 
350         fn value(comptime attr_id: u32, ptr: *const u8) Attribute {
351             return .{
352                 .attr_id = Attribute.attrIdFromInt(attr_id),
353                 .impl = ptr,
354                 .abstract = &abstract,
355             };
356         }
357     };
358 
359     const alpha = TestAttr.value(1, &TestAttr.alpha);
360     const beta = TestAttr.value(2, &TestAttr.beta);
361     const gamma = TestAttr.value(3, &TestAttr.gamma);
362 
363     var list = NamedAttributeList{};
364     defer list.deinit(testing.allocator);
365 
366     try list.ensureTotalCapacity(testing.allocator, 8);
367     try testing.expect(list.capacity() >= 8);
368 
369     try testing.expect(try list.set(testing.allocator, "gamma", gamma) == null);
370     try testing.expect(try list.set(testing.allocator, "alpha", alpha) == null);
371     try testing.expect(try list.set(testing.allocator, "beta", beta) == null);
372 
373     try testing.expectEqualStrings("alpha", list.items()[0].name);
374     try testing.expectEqualStrings("beta", list.items()[1].name);
375     try testing.expectEqualStrings("gamma", list.items()[2].name);
376     try testing.expectEqual(alpha.impl, list.get("alpha").?.impl);
377 
378     const previous = (try list.set(testing.allocator, "beta", gamma)).?;
379     try testing.expectEqual(beta.impl, previous.impl);
380     try testing.expectEqual(gamma.impl, list.get("beta").?.impl);
381 
382     const erased = list.erase("alpha").?;
383     try testing.expectEqual(alpha.impl, erased.impl);
384     try testing.expect(list.erase("missing") == null);
385     try testing.expect(list.get("alpha") == null);
386     try testing.expectEqual(@as(usize, 2), list.items().len);
387 }
388 
389 test "NamedAttributeList stores small dictionaries without allocation" {
390     const testing = std.testing;
391     const TestAttr = struct {
392         const abstract = interfaces.AbstractAttribute{
393             .attr_id = 1,
394             .name = "test.attr",
395             .interfaces = &.{},
396         };
397         const alpha: u8 = 1;
398 
399         fn value(ptr: *const u8) Attribute {
400             return .{
401                 .attr_id = Attribute.attrIdFromInt(1),
402                 .impl = ptr,
403                 .abstract = &abstract,
404             };
405         }
406     };
407 
408     const attr = TestAttr.value(&TestAttr.alpha);
409     var list = NamedAttributeList{};
410     defer list.deinit(testing.failing_allocator);
411 
412     try testing.expect(try list.set(testing.failing_allocator, "delta", attr) == null);
413     try testing.expect(try list.set(testing.failing_allocator, "alpha", attr) == null);
414     try testing.expect(try list.set(testing.failing_allocator, "gamma", attr) == null);
415     try testing.expect(try list.set(testing.failing_allocator, "beta", attr) == null);
416 
417     const attrs = list.items();
418     try testing.expectEqual(@as(usize, 4), attrs.len);
419     try testing.expectEqualStrings("alpha", attrs[0].name);
420     try testing.expectEqualStrings("beta", attrs[1].name);
421     try testing.expectEqualStrings("delta", attrs[2].name);
422     try testing.expectEqualStrings("gamma", attrs[3].name);
423 
424     try testing.expectError(error.OutOfMemory, list.set(testing.failing_allocator, "epsilon", attr));
425     try testing.expectEqual(@as(usize, 4), list.items().len);
426 }
427 
428 test "NamedAttributeList grows spilled dictionaries geometrically" {
429     const testing = std.testing;
430     const TestAttr = struct {
431         const abstract = interfaces.AbstractAttribute{
432             .attr_id = 1,
433             .name = "test.attr",
434             .interfaces = &.{},
435         };
436         const payload: u8 = 1;
437 
438         fn value() Attribute {
439             return .{
440                 .attr_id = Attribute.attrIdFromInt(1),
441                 .impl = &payload,
442                 .abstract = &abstract,
443             };
444         }
445     };
446 
447     var failing = testing.FailingAllocator.init(testing.allocator, .{});
448     var list = NamedAttributeList{};
449     defer list.deinit(failing.allocator());
450 
451     const names = [_][]const u8{
452         "alpha", "beta", "gamma", "delta", "epsilon",
453         "zeta",  "eta",  "theta", "iota",
454     };
455     for (names[0..5]) |name| {
456         try testing.expect(try list.set(failing.allocator(), name, TestAttr.value()) == null);
457     }
458     try testing.expectEqual(@as(usize, 8), list.capacity());
459 
460     failing.fail_index = failing.alloc_index;
461     failing.resize_fail_index = failing.resize_index;
462     for (names[5..8]) |name| {
463         try testing.expect(try list.set(failing.allocator(), name, TestAttr.value()) == null);
464     }
465     try testing.expectError(
466         error.OutOfMemory,
467         list.set(failing.allocator(), names[8], TestAttr.value()),
468     );
469     try testing.expectEqual(@as(usize, 8), list.items().len);
470 
471     failing.fail_index = std.math.maxInt(usize);
472     failing.resize_fail_index = std.math.maxInt(usize);
473     try testing.expect(try list.set(
474         failing.allocator(),
475         names[8],
476         TestAttr.value(),
477     ) == null);
478     try testing.expectEqual(@as(usize, 16), list.capacity());
479 }
480 
481 test "Attribute.cast checks comptime storage type" {
482     const testing = std.testing;
483 
484     var context_token: u8 = 0;
485     const integer_abstract = interfaces.AbstractAttribute{
486         .attr_id = @backingInt(Attribute.AttrID.invalid) + 1,
487         .name = builtin_attr_names.integer,
488         .interfaces = &.{},
489     };
490     const string_abstract = interfaces.AbstractAttribute{
491         .attr_id = @backingInt(Attribute.AttrID.invalid) + 2,
492         .name = builtin_attr_names.string,
493         .interfaces = &.{},
494     };
495     const dialect_abstract = interfaces.AbstractAttribute{
496         .attr_id = @backingInt(Attribute.AttrID.invalid) + 3,
497         .name = "test.flag",
498         .interfaces = &.{},
499     };
500 
501     const integer_storage = Attribute.IntegerAttr{
502         .value = 42,
503         .width = 64,
504         .is_signed = true,
505         .context = &context_token,
506     };
507     const string_storage = Attribute.StringAttr{
508         .value = "name",
509         .context = &context_token,
510     };
511     const dialect_storage = Attribute.DialectAttr{
512         .payload = "payload",
513         .context = &context_token,
514     };
515 
516     const int_attr = Attribute{
517         .attr_id = Attribute.attrIdFromInt(integer_abstract.attr_id),
518         .impl = &integer_storage,
519         .abstract = &integer_abstract,
520     };
521     const string_attr = Attribute{
522         .attr_id = Attribute.attrIdFromInt(string_abstract.attr_id),
523         .impl = &string_storage,
524         .abstract = &string_abstract,
525     };
526     const dialect_attr = Attribute{
527         .attr_id = Attribute.attrIdFromInt(dialect_abstract.attr_id),
528         .impl = &dialect_storage,
529         .abstract = &dialect_abstract,
530     };
531 
532     try testing.expect(int_attr.hasStorageType(Attribute.IntegerAttr));
533     try testing.expect(int_attr.cast(Attribute.IntegerAttr) != null);
534     try testing.expect(int_attr.cast(Attribute.StringAttr) == null);
535     try testing.expect(int_attr.cast(Attribute.DialectAttr) == null);
536 
537     try testing.expect(string_attr.cast(Attribute.StringAttr) != null);
538     try testing.expect(string_attr.cast(Attribute.IntegerAttr) == null);
539 
540     try testing.expect(dialect_attr.hasStorageType(Attribute.DialectAttr));
541     try testing.expect(dialect_attr.cast(Attribute.DialectAttr) != null);
542     try testing.expect(dialect_attr.cast(Attribute.BoolAttr) == null);
543 }
544 
545 pub const builtin_attr_names = struct {
546     pub const integer = "builtin.integer";
547     pub const float_ = "builtin.float";
548     pub const bool_ = "builtin.bool";
549     pub const string = "builtin.string";
550     pub const symbol_ref = "builtin.symbol_ref";
551     pub const string_list = "builtin.string_list";
552     pub const type_list = "builtin.type_list";
553     pub const array = "builtin.array";
554 };
555 
556 const builtin_attr_name_values = [_][]const u8{
557     builtin_attr_names.integer,
558     builtin_attr_names.float_,
559     builtin_attr_names.bool_,
560     builtin_attr_names.string,
561     builtin_attr_names.symbol_ref,
562     builtin_attr_names.string_list,
563     builtin_attr_names.type_list,
564     builtin_attr_names.array,
565 };
566 
567 pub fn isBuiltinAttributeName(name: []const u8) bool {
568     inline for (builtin_attr_name_values) |builtin_name| {
569         if (std.mem.eql(u8, name, builtin_name)) return true;
570     }
571     return false;
572 }
573 
574 fn storageTypeMatchesName(comptime T: type, name: []const u8) bool {
575     if (T == Attribute.DialectAttr) {
576         return name.len > 0 and !isBuiltinAttributeName(name);
577     }
578     if (T == Attribute.IntegerAttr) return std.mem.eql(u8, name, builtin_attr_names.integer);
579     if (T == Attribute.FloatAttr) return std.mem.eql(u8, name, builtin_attr_names.float_);
580     if (T == Attribute.BoolAttr) return std.mem.eql(u8, name, builtin_attr_names.bool_);
581     if (T == Attribute.StringAttr) return std.mem.eql(u8, name, builtin_attr_names.string);
582     if (T == Attribute.SymbolRefAttr) return std.mem.eql(u8, name, builtin_attr_names.symbol_ref);
583     if (T == Attribute.StringListAttr) return std.mem.eql(u8, name, builtin_attr_names.string_list);
584     if (T == Attribute.TypeListAttr) return std.mem.eql(u8, name, builtin_attr_names.type_list);
585     if (T == Attribute.ArrayAttr) return std.mem.eql(u8, name, builtin_attr_names.array);
586     @compileError("unsupported attribute storage type: " ++ @typeName(T));
587 }
588 
589 const EqlVTable = interfaces.AttributeEqualInterface.VTable;
590 
591 fn dialect_attr_eql(self_impl: *const anyopaque, other_impl: *const anyopaque) bool {
592     const a: *const Attribute.DialectAttr = @ptrCast(@alignCast(self_impl));
593     const b: *const Attribute.DialectAttr = @ptrCast(@alignCast(other_impl));
594     return std.mem.eql(u8, a.payload, b.payload);
595 }
596 
597 fn integer_attr_eql(self_impl: *const anyopaque, other_impl: *const anyopaque) bool {
598     const a: *const Attribute.IntegerAttr = @ptrCast(@alignCast(self_impl));
599     const b: *const Attribute.IntegerAttr = @ptrCast(@alignCast(other_impl));
600     return a.value == b.value and a.width == b.width and a.is_signed == b.is_signed;
601 }
602 
603 fn float_attr_eql(self_impl: *const anyopaque, other_impl: *const anyopaque) bool {
604     const a: *const Attribute.FloatAttr = @ptrCast(@alignCast(self_impl));
605     const b: *const Attribute.FloatAttr = @ptrCast(@alignCast(other_impl));
606     return @as(u64, @bitCast(a.value)) == @as(u64, @bitCast(b.value)) and a.width == b.width;
607 }
608 
609 fn bool_attr_eql(self_impl: *const anyopaque, other_impl: *const anyopaque) bool {
610     const a: *const Attribute.BoolAttr = @ptrCast(@alignCast(self_impl));
611     const b: *const Attribute.BoolAttr = @ptrCast(@alignCast(other_impl));
612     return a.value == b.value;
613 }
614 
615 fn string_attr_eql(self_impl: *const anyopaque, other_impl: *const anyopaque) bool {
616     const a: *const Attribute.StringAttr = @ptrCast(@alignCast(self_impl));
617     const b: *const Attribute.StringAttr = @ptrCast(@alignCast(other_impl));
618     return std.mem.eql(u8, a.value, b.value);
619 }
620 
621 fn symbol_ref_attr_eql(self_impl: *const anyopaque, other_impl: *const anyopaque) bool {
622     const a: *const Attribute.SymbolRefAttr = @ptrCast(@alignCast(self_impl));
623     const b: *const Attribute.SymbolRefAttr = @ptrCast(@alignCast(other_impl));
624     if (!std.mem.eql(u8, a.root_reference, b.root_reference)) return false;
625     if (a.nested_references.len != b.nested_references.len) return false;
626     for (a.nested_references, b.nested_references) |lhs, rhs| {
627         if (!std.mem.eql(u8, lhs, rhs)) return false;
628     }
629     return true;
630 }
631 
632 fn write_symbol_ref_attr(
633     self_impl: *const anyopaque,
634     writer: *std.Io.Writer,
635 ) std.Io.Writer.Error!void {
636     const attr: *const Attribute.SymbolRefAttr = @ptrCast(@alignCast(self_impl));
637     try writer.print("@{s}", .{attr.root_reference});
638     for (attr.nested_references) |nested| {
639         try writer.print("::@{s}", .{nested});
640     }
641 }
642 
643 fn string_list_attr_eql(self_impl: *const anyopaque, other_impl: *const anyopaque) bool {
644     const a: *const Attribute.StringListAttr = @ptrCast(@alignCast(self_impl));
645     const b: *const Attribute.StringListAttr = @ptrCast(@alignCast(other_impl));
646     if (a.values.len != b.values.len) return false;
647     for (a.values, b.values) |lhs, rhs| {
648         if (!std.mem.eql(u8, lhs, rhs)) return false;
649     }
650     return true;
651 }
652 
653 fn type_list_attr_eql(self_impl: *const anyopaque, other_impl: *const anyopaque) bool {
654     const a: *const Attribute.TypeListAttr = @ptrCast(@alignCast(self_impl));
655     const b: *const Attribute.TypeListAttr = @ptrCast(@alignCast(other_impl));
656     if (a.values.len != b.values.len) return false;
657     for (a.values, b.values) |lhs, rhs| {
658         if (!lhs.eql(rhs)) return false;
659     }
660     return true;
661 }
662 
663 fn array_attr_eql(self_impl: *const anyopaque, other_impl: *const anyopaque) bool {
664     const a: *const Attribute.ArrayAttr = @ptrCast(@alignCast(self_impl));
665     const b: *const Attribute.ArrayAttr = @ptrCast(@alignCast(other_impl));
666     if (a.values.len != b.values.len) return false;
667     for (a.values, b.values) |lhs, rhs| {
668         if (!lhs.eql(rhs)) return false;
669     }
670     return true;
671 }
672 
673 fn array_attr_count(attr_ptr: *const anyopaque) usize {
674     const attr: *const Attribute.ArrayAttr = @ptrCast(@alignCast(attr_ptr));
675     return attr.values.len;
676 }
677 
678 fn array_attr_element(attr_ptr: *const anyopaque, index: usize) ?Attribute {
679     const attr: *const Attribute.ArrayAttr = @ptrCast(@alignCast(attr_ptr));
680     if (index >= attr.values.len) return null;
681     return attr.values[index];
682 }
683 
684 pub const dialect_attr_eql_vtable = EqlVTable{
685     .eql = dialect_attr_eql,
686 };
687 
688 pub const integer_attr_eql_vtable = EqlVTable{
689     .eql = integer_attr_eql,
690 };
691 
692 pub const float_attr_eql_vtable = EqlVTable{
693     .eql = float_attr_eql,
694 };
695 
696 pub const bool_attr_eql_vtable = EqlVTable{
697     .eql = bool_attr_eql,
698 };
699 
700 pub const string_attr_eql_vtable = EqlVTable{
701     .eql = string_attr_eql,
702 };
703 
704 pub const symbol_ref_attr_eql_vtable = EqlVTable{
705     .eql = symbol_ref_attr_eql,
706 };
707 
708 pub const symbol_ref_attr_print_vtable = interfaces.AttributePrintInterface.VTable{
709     .print = write_symbol_ref_attr,
710 };
711 
712 pub const string_list_attr_eql_vtable = EqlVTable{
713     .eql = string_list_attr_eql,
714 };
715 
716 pub const type_list_attr_eql_vtable = EqlVTable{
717     .eql = type_list_attr_eql,
718 };
719 
720 pub const array_attr_eql_vtable = EqlVTable{
721     .eql = array_attr_eql,
722 };
723 
724 pub const array_attr_array_vtable = interfaces.AttributeArrayInterface.VTable{
725     .getCount = array_attr_count,
726     .getElement = array_attr_element,
727 };
728 
729 pub const default_abstract = interfaces.AbstractAttribute{
730     .attr_id = 0,
731     .name = "",
732     .interfaces = &.{},
733 };