lib/choir/src/core/context/attrs.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const attribute = @import("../root.zig").attribute;
  3 const Attribute = attribute.Attribute;
  4 const Operation = @import("../root.zig").Operation;
  5 const Type = @import("../root.zig").Type;
  6 const interfaces = @import("../root.zig").interfaces;
  7 const attrs = @import("../root.zig").attrs;
  8 const dialect_ctx = @import("dialects.zig");
  9 const freeze_ctx = @import("freeze.zig");
 10 const memory_ctx = @import("memory.zig");
 11 const transactions = @import("transactions.zig");
 12 
 13 pub fn lookupAttributeType(
 14     ctx: anytype,
 15     attr_name: []const u8,
 16 ) ?*const interfaces.AbstractAttribute {
 17     return ctx.attr_interner.registry.lookup(attr_name);
 18 }
 19 
 20 pub fn registerAttributeType(
 21     ctx: anytype,
 22     attr_name: []const u8,
 23     iface_entries: []const interfaces.InterfaceEntry,
 24 ) (@TypeOf(ctx.*).Error || interfaces.AttributeRegistry.RegisterError)!*const interfaces.AbstractAttribute {
 25     try freeze_ctx.requireMutable(ctx);
 26 
 27     const name_struct = Operation.OperationName.init(attr_name);
 28     const ns = name_struct.getDialectNamespace();
 29     if (ns.len > 0) {
 30         if (ctx.dialect_registry.load_state.get(ns)) |state| {
 31             if (state == .loaded) {
 32                 _ = dialect_ctx.getOrLoadDialect(ctx, ns) catch {};
 33             }
 34         } else {
 35             _ = dialect_ctx.getOrLoadDialect(ctx, ns) catch {};
 36         }
 37     }
 38 
 39     const existed_before = ctx.attr_interner.registry.lookup(attr_name) != null;
 40 
 41     const attr = ctx.attr_interner.registry.registerAttribute(attr_name, iface_entries) catch |err| switch (err) {
 42         error.DuplicateAttribute => blk: {
 43             for (iface_entries) |entry| {
 44                 ctx.attr_interner.registry.registerOrReplaceInterface(attr_name, entry) catch |e| switch (e) {
 45                     error.UnknownAttribute => unreachable,
 46                     error.OutOfMemory => return error.OutOfMemory,
 47                 };
 48             }
 49             break :blk ctx.attr_interner.registry.lookup(attr_name).?;
 50         },
 51         else => return err,
 52     };
 53 
 54     if (!existed_before) {
 55         try transactions.recordCreatedAttribute(ctx, attr_name, true);
 56     }
 57 
 58     return attr;
 59 }
 60 
 61 pub fn registerAttributeInterfaceExternal(
 62     ctx: anytype,
 63     attr_name: []const u8,
 64     entry: interfaces.InterfaceEntry,
 65 ) (@TypeOf(ctx.*).Error || std.mem.Allocator.Error)!void {
 66     try freeze_ctx.requireMutable(ctx);
 67     if (ctx.attr_interner.registry.lookup(attr_name) == null) {
 68         _ = try registerAttributeType(ctx, attr_name, &.{});
 69     }
 70     ctx.attr_interner.registry.registerOrReplaceInterface(attr_name, entry) catch |err| switch (err) {
 71         error.UnknownAttribute => unreachable,
 72         error.OutOfMemory => return error.OutOfMemory,
 73     };
 74 }
 75 
 76 pub fn getDialectAttr(
 77     ctx: anytype,
 78     full_name: []const u8,
 79     payload: []const u8,
 80 ) !Attribute {
 81     const abstract = ctx.attr_interner.registry.lookup(full_name) orelse blk: {
 82         const iface = interfaces.AttributeEqualInterface.entry(&attribute.dialect_attr_eql_vtable);
 83         const interfaces_list = &.{iface};
 84         break :blk try registerAttributeType(ctx, full_name, interfaces_list);
 85     };
 86 
 87     const lookup_key = attrs.DialectAttrKey{
 88         .attr_id = abstract.attr_id,
 89         .payload = payload,
 90     };
 91     if (ctx.attr_interner.storage.dialect_attrs.get(lookup_key)) |attr| {
 92         return attr;
 93     }
 94 
 95     try freeze_ctx.requireSingleThreadedMutation(ctx);
 96 
 97     const payload_copy = try memory_ctx.segmentAllocator(ctx, .attribute_payloads).dupe(u8, payload);
 98     errdefer memory_ctx.segmentAllocator(ctx, .attribute_payloads).free(payload_copy);
 99     const storage = try memory_ctx.segmentAllocator(ctx, .attribute_payloads).create(Attribute.DialectAttr);
100     errdefer memory_ctx.segmentAllocator(ctx, .attribute_payloads).destroy(storage);
101     storage.* = .{ .payload = payload_copy, .context = ctx };
102 
103     const a = Attribute{
104         .attr_id = Attribute.attrIdFromInt(abstract.attr_id),
105         .impl = storage,
106         .abstract = abstract,
107     };
108 
109     try ctx.attr_interner.storage.dialect_attrs.put(
110         .{ .attr_id = abstract.attr_id, .payload = payload_copy },
111         a,
112     );
113     return a;
114 }
115 
116 pub fn getIntegerAttr(
117     ctx: anytype,
118     value: i64,
119     width: u8,
120     is_signed: bool,
121 ) !Attribute {
122     const lookup_key = attrs.IntegerAttrKey{
123         .value = value,
124         .width = width,
125         .is_signed = is_signed,
126     };
127 
128     if (ctx.attr_interner.storage.integer_attrs.get(lookup_key)) |attr| {
129         return attr;
130     }
131 
132     try freeze_ctx.requireSingleThreadedMutation(ctx);
133 
134     const abstract = ctx.attr_interner.registry.lookup(attribute.builtin_attr_names.integer) orelse blk: {
135         const iface = interfaces.AttributeEqualInterface.entry(&attribute.integer_attr_eql_vtable);
136         break :blk try registerAttributeType(ctx, attribute.builtin_attr_names.integer, &.{iface});
137     };
138 
139     const storage = try memory_ctx.segmentAllocator(ctx, .attribute_payloads).create(Attribute.IntegerAttr);
140     errdefer memory_ctx.segmentAllocator(ctx, .attribute_payloads).destroy(storage);
141     storage.* = .{
142         .value = value,
143         .width = width,
144         .is_signed = is_signed,
145         .context = ctx,
146     };
147 
148     const a = Attribute{
149         .attr_id = Attribute.attrIdFromInt(abstract.attr_id),
150         .impl = storage,
151         .abstract = abstract,
152     };
153 
154     try ctx.attr_interner.storage.integer_attrs.put(lookup_key, a);
155     return a;
156 }
157 
158 pub fn getI64Attr(ctx: anytype, value: i64) !Attribute {
159     return getIntegerAttr(ctx, value, 64, true);
160 }
161 
162 pub fn getI32Attr(ctx: anytype, value: i32) !Attribute {
163     return getIntegerAttr(ctx, value, 32, true);
164 }
165 
166 pub fn getFloatAttr(
167     ctx: anytype,
168     value: f64,
169     width: u8,
170 ) !Attribute {
171     const lookup_key = attrs.FloatAttrKey{
172         .value_bits = @bitCast(value),
173         .width = width,
174     };
175 
176     if (ctx.attr_interner.storage.float_attrs.get(lookup_key)) |attr| {
177         return attr;
178     }
179 
180     try freeze_ctx.requireSingleThreadedMutation(ctx);
181 
182     const abstract = ctx.attr_interner.registry.lookup(attribute.builtin_attr_names.float_) orelse blk: {
183         const iface = interfaces.AttributeEqualInterface.entry(&attribute.float_attr_eql_vtable);
184         break :blk try registerAttributeType(ctx, attribute.builtin_attr_names.float_, &.{iface});
185     };
186 
187     const storage = try memory_ctx.segmentAllocator(ctx, .attribute_payloads).create(Attribute.FloatAttr);
188     errdefer memory_ctx.segmentAllocator(ctx, .attribute_payloads).destroy(storage);
189     storage.* = .{
190         .value = value,
191         .width = width,
192         .context = ctx,
193     };
194 
195     const a = Attribute{
196         .attr_id = Attribute.attrIdFromInt(abstract.attr_id),
197         .impl = storage,
198         .abstract = abstract,
199     };
200 
201     try ctx.attr_interner.storage.float_attrs.put(lookup_key, a);
202     return a;
203 }
204 
205 pub fn getF64Attr(ctx: anytype, value: f64) !Attribute {
206     return getFloatAttr(ctx, value, 64);
207 }
208 
209 pub fn getF32Attr(ctx: anytype, value: f32) !Attribute {
210     return getFloatAttr(ctx, value, 32);
211 }
212 
213 pub fn getBoolAttr(ctx: anytype, value: bool) !Attribute {
214     if (ctx.attr_interner.storage.bool_attrs.get(value)) |attr| {
215         return attr;
216     }
217 
218     try freeze_ctx.requireSingleThreadedMutation(ctx);
219 
220     const abstract = ctx.attr_interner.registry.lookup(attribute.builtin_attr_names.bool_) orelse blk: {
221         const iface = interfaces.AttributeEqualInterface.entry(&attribute.bool_attr_eql_vtable);
222         break :blk try registerAttributeType(ctx, attribute.builtin_attr_names.bool_, &.{iface});
223     };
224 
225     const storage = try memory_ctx.segmentAllocator(ctx, .attribute_payloads).create(Attribute.BoolAttr);
226     errdefer memory_ctx.segmentAllocator(ctx, .attribute_payloads).destroy(storage);
227     storage.* = .{
228         .value = value,
229         .context = ctx,
230     };
231 
232     const a = Attribute{
233         .attr_id = Attribute.attrIdFromInt(abstract.attr_id),
234         .impl = storage,
235         .abstract = abstract,
236     };
237 
238     try ctx.attr_interner.storage.bool_attrs.put(value, a);
239     return a;
240 }
241 
242 pub fn getStringAttr(ctx: anytype, value: []const u8) !Attribute {
243     const lookup_key = attrs.StringAttrKey{ .value = value };
244 
245     if (ctx.attr_interner.storage.string_attrs.get(lookup_key)) |attr| {
246         return attr;
247     }
248 
249     try freeze_ctx.requireSingleThreadedMutation(ctx);
250 
251     const abstract = ctx.attr_interner.registry.lookup(attribute.builtin_attr_names.string) orelse blk: {
252         const iface = interfaces.AttributeEqualInterface.entry(&attribute.string_attr_eql_vtable);
253         break :blk try registerAttributeType(ctx, attribute.builtin_attr_names.string, &.{iface});
254     };
255 
256     const value_copy = try memory_ctx.segmentAllocator(ctx, .attribute_payloads).dupe(u8, value);
257     errdefer memory_ctx.segmentAllocator(ctx, .attribute_payloads).free(value_copy);
258 
259     const storage = try memory_ctx.segmentAllocator(ctx, .attribute_payloads).create(Attribute.StringAttr);
260     errdefer memory_ctx.segmentAllocator(ctx, .attribute_payloads).destroy(storage);
261     storage.* = .{
262         .value = value_copy,
263         .context = ctx,
264     };
265 
266     const a = Attribute{
267         .attr_id = Attribute.attrIdFromInt(abstract.attr_id),
268         .impl = storage,
269         .abstract = abstract,
270     };
271 
272     try ctx.attr_interner.storage.string_attrs.put(.{ .value = value_copy }, a);
273     return a;
274 }
275 
276 pub fn getSymbolRefAttr(
277     ctx: anytype,
278     root_reference: []const u8,
279     nested_references: []const []const u8,
280 ) !Attribute {
281     const lookup_key = attrs.SymbolRefAttrKey{
282         .root_reference = root_reference,
283         .nested_references = nested_references,
284     };
285 
286     if (ctx.attr_interner.storage.symbol_ref_attrs.get(lookup_key)) |attr| {
287         return attr;
288     }
289 
290     try freeze_ctx.requireSingleThreadedMutation(ctx);
291 
292     const abstract = ctx.attr_interner.registry.lookup(attribute.builtin_attr_names.symbol_ref) orelse blk: {
293         const equal_iface = interfaces.AttributeEqualInterface.entry(&attribute.symbol_ref_attr_eql_vtable);
294         const print_iface = interfaces.AttributePrintInterface.entry(&attribute.symbol_ref_attr_print_vtable);
295         break :blk try registerAttributeType(ctx, attribute.builtin_attr_names.symbol_ref, &.{ equal_iface, print_iface });
296     };
297 
298     const root_copy = try memory_ctx.segmentAllocator(ctx, .attribute_payloads).dupe(u8, root_reference);
299     errdefer memory_ctx.segmentAllocator(ctx, .attribute_payloads).free(root_copy);
300 
301     const nested_copy = try dupeStringList(memory_ctx.segmentAllocator(ctx, .attribute_payloads), nested_references);
302     errdefer {
303         var index = nested_copy.len;
304         while (index > 0) {
305             index -= 1;
306             memory_ctx.segmentAllocator(ctx, .attribute_payloads).free(nested_copy[index]);
307         }
308         if (nested_copy.len > 0) memory_ctx.segmentAllocator(ctx, .attribute_payloads).free(nested_copy);
309     }
310 
311     const storage = try memory_ctx.segmentAllocator(ctx, .attribute_payloads).create(Attribute.SymbolRefAttr);
312     errdefer memory_ctx.segmentAllocator(ctx, .attribute_payloads).destroy(storage);
313     storage.* = .{
314         .root_reference = root_copy,
315         .nested_references = nested_copy,
316         .context = ctx,
317     };
318 
319     const a = Attribute{
320         .attr_id = Attribute.attrIdFromInt(abstract.attr_id),
321         .impl = storage,
322         .abstract = abstract,
323     };
324 
325     try ctx.attr_interner.storage.symbol_ref_attrs.put(.{
326         .root_reference = root_copy,
327         .nested_references = nested_copy,
328     }, a);
329     return a;
330 }
331 
332 pub fn getFlatSymbolRefAttr(ctx: anytype, root_reference: []const u8) !Attribute {
333     return getSymbolRefAttr(ctx, root_reference, &.{});
334 }
335 
336 fn dupeStringList(allocator: std.mem.Allocator, values: []const []const u8) ![]const []const u8 {
337     if (values.len == 0) return &[_][]const u8{};
338 
339     const values_copy = try allocator.alloc([]const u8, values.len);
340     errdefer allocator.free(values_copy);
341 
342     var initialized: usize = 0;
343     errdefer {
344         while (initialized > 0) {
345             initialized -= 1;
346             allocator.free(values_copy[initialized]);
347         }
348     }
349 
350     for (values, 0..) |value, index| {
351         values_copy[index] = try allocator.dupe(u8, value);
352         initialized += 1;
353     }
354 
355     return values_copy;
356 }
357 
358 pub fn getStringListAttr(ctx: anytype, values: []const []const u8) !Attribute {
359     const lookup_key = attrs.StringListAttrKey{ .values = values };
360 
361     if (ctx.attr_interner.storage.string_list_attrs.get(lookup_key)) |attr| {
362         return attr;
363     }
364 
365     try freeze_ctx.requireSingleThreadedMutation(ctx);
366 
367     const abstract = ctx.attr_interner.registry.lookup(attribute.builtin_attr_names.string_list) orelse blk: {
368         const iface = interfaces.AttributeEqualInterface.entry(&attribute.string_list_attr_eql_vtable);
369         break :blk try registerAttributeType(ctx, attribute.builtin_attr_names.string_list, &.{iface});
370     };
371 
372     const values_copy = try dupeStringList(memory_ctx.segmentAllocator(ctx, .attribute_payloads), values);
373     errdefer {
374         var index = values_copy.len;
375         while (index > 0) {
376             index -= 1;
377             memory_ctx.segmentAllocator(ctx, .attribute_payloads).free(values_copy[index]);
378         }
379         if (values_copy.len > 0) memory_ctx.segmentAllocator(ctx, .attribute_payloads).free(values_copy);
380     }
381 
382     const storage = try memory_ctx.segmentAllocator(ctx, .attribute_payloads).create(Attribute.StringListAttr);
383     errdefer memory_ctx.segmentAllocator(ctx, .attribute_payloads).destroy(storage);
384     storage.* = .{
385         .values = values_copy,
386         .context = ctx,
387     };
388 
389     const a = Attribute{
390         .attr_id = Attribute.attrIdFromInt(abstract.attr_id),
391         .impl = storage,
392         .abstract = abstract,
393     };
394 
395     try ctx.attr_interner.storage.string_list_attrs.put(.{ .values = values_copy }, a);
396     return a;
397 }
398 
399 pub fn getTypeListAttr(ctx: anytype, values: []const Type) !Attribute {
400     const lookup_key = attrs.TypeListAttrKey{ .values = values };
401 
402     if (ctx.attr_interner.storage.type_list_attrs.get(lookup_key)) |attr| {
403         return attr;
404     }
405 
406     try freeze_ctx.requireSingleThreadedMutation(ctx);
407 
408     const abstract = ctx.attr_interner.registry.lookup(attribute.builtin_attr_names.type_list) orelse blk: {
409         const iface = interfaces.AttributeEqualInterface.entry(&attribute.type_list_attr_eql_vtable);
410         break :blk try registerAttributeType(ctx, attribute.builtin_attr_names.type_list, &.{iface});
411     };
412 
413     const values_copy = if (values.len > 0)
414         try memory_ctx.segmentAllocator(ctx, .attribute_payloads).dupe(Type, values)
415     else
416         &[_]Type{};
417     errdefer if (values_copy.len > 0) memory_ctx.segmentAllocator(ctx, .attribute_payloads).free(values_copy);
418 
419     const storage = try memory_ctx.segmentAllocator(ctx, .attribute_payloads).create(Attribute.TypeListAttr);
420     errdefer memory_ctx.segmentAllocator(ctx, .attribute_payloads).destroy(storage);
421     storage.* = .{
422         .values = values_copy,
423         .context = ctx,
424     };
425 
426     const a = Attribute{
427         .attr_id = Attribute.attrIdFromInt(abstract.attr_id),
428         .impl = storage,
429         .abstract = abstract,
430     };
431 
432     try ctx.attr_interner.storage.type_list_attrs.put(.{ .values = values_copy }, a);
433     return a;
434 }
435 
436 pub fn getArrayAttr(ctx: anytype, values: []const Attribute) !Attribute {
437     const lookup_key = attrs.ArrayAttrKey{ .values = values };
438 
439     if (ctx.attr_interner.storage.array_attrs.get(lookup_key)) |attr| {
440         return attr;
441     }
442 
443     try freeze_ctx.requireSingleThreadedMutation(ctx);
444 
445     const abstract = ctx.attr_interner.registry.lookup(attribute.builtin_attr_names.array) orelse blk: {
446         const equal_iface = interfaces.AttributeEqualInterface.entry(&attribute.array_attr_eql_vtable);
447         const array_iface = interfaces.AttributeArrayInterface.entry(&attribute.array_attr_array_vtable);
448         break :blk try registerAttributeType(ctx, attribute.builtin_attr_names.array, &.{ equal_iface, array_iface });
449     };
450 
451     const values_copy = if (values.len > 0)
452         try memory_ctx.segmentAllocator(ctx, .attribute_payloads).dupe(Attribute, values)
453     else
454         &[_]Attribute{};
455     errdefer if (values_copy.len > 0) memory_ctx.segmentAllocator(ctx, .attribute_payloads).free(values_copy);
456 
457     const storage = try memory_ctx.segmentAllocator(ctx, .attribute_payloads).create(Attribute.ArrayAttr);
458     errdefer memory_ctx.segmentAllocator(ctx, .attribute_payloads).destroy(storage);
459     storage.* = .{
460         .values = values_copy,
461         .context = ctx,
462     };
463 
464     const a = Attribute{
465         .attr_id = Attribute.attrIdFromInt(abstract.attr_id),
466         .impl = storage,
467         .abstract = abstract,
468     };
469 
470     try ctx.attr_interner.storage.array_attrs.put(.{ .values = values_copy }, a);
471     return a;
472 }
473 
474 test "Context attribute store cleanup" {
475     const testing = std.testing;
476     const Context = @import("root.zig").Context;
477     var ctx = try Context.init(testing.allocator, Context.Limits.testing);
478     defer ctx.deinit(testing.allocator);
479 
480     _ = try ctx.getDialectAttr("test.unit", "");
481     _ = try ctx.getDialectAttr("test.bool", &[_]u8{1});
482     _ = try ctx.getDialectAttr("test.bool", &[_]u8{0});
483     _ = try ctx.getDialectAttr("test.int", "42");
484     _ = try ctx.getDialectAttr("test.int", "100");
485     _ = try ctx.getDialectAttr("test.string", "test");
486     _ = try ctx.getDialectAttr("test.string", "another");
487     _ = try ctx.getDialectAttr("test.type", "test.ty");
488 }
489 
490 test "Context attribute type registration upgrades existing attributes" {
491     const testing = std.testing;
492     const Context = @import("root.zig").Context;
493     var ctx = try Context.init(testing.allocator, Context.Limits.testing);
494     defer ctx.deinit(testing.allocator);
495 
496     const attr = try ctx.getDialectAttr("test.foo", "payload");
497 
498     const before = try std.fmt.allocPrint(testing.allocator, "{f}", .{attr});
499     defer testing.allocator.free(before);
500     try testing.expectEqualStrings("#attr<test.foo>", before);
501 
502     const Print = struct {
503         fn print(_: *const anyopaque, writer: *std.Io.Writer) std.Io.Writer.Error!void {
504             try writer.writeAll("foo");
505         }
506     };
507     const vtable = interfaces.AttributePrintInterface.vtableFor(Print.print);
508     const ifaces = [_]interfaces.InterfaceEntry{
509         interfaces.AttributePrintInterface.entry(vtable),
510     };
511 
512     _ = try ctx.registerAttributeType("test.foo", &ifaces);
513 
514     const after = try std.fmt.allocPrint(testing.allocator, "{f}", .{attr});
515     defer testing.allocator.free(after);
516     try testing.expectEqualStrings("foo", after);
517 }
518 
519 test "Context getDialectAttr creates and uniques dialect attributes" {
520     const testing = std.testing;
521     const Context = @import("root.zig").Context;
522     var ctx = try Context.init(testing.allocator, Context.Limits.testing);
523     defer ctx.deinit(testing.allocator);
524 
525     const attr1 = try ctx.getDialectAttr("test.foo", "payload");
526     const attr2 = try ctx.getDialectAttr("test.foo", "payload");
527     const attr3 = try ctx.getDialectAttr("test.foo", "other");
528     const attr4 = try ctx.getDialectAttr("test.bar", "payload");
529 
530     try testing.expectEqual(attr1.impl, attr2.impl);
531 
532     try testing.expect(attr1.impl != attr3.impl);
533     try testing.expect(attr1.impl != attr4.impl);
534 
535     try testing.expectEqualStrings("test.foo", attr1.abstract.name);
536     const dialect_attr = attr1.cast(Attribute.DialectAttr).?;
537     try testing.expectEqualStrings("payload", dialect_attr.payload);
538 }
539 
540 test "Context attributes cast by storage type" {
541     const testing = std.testing;
542     const Context = @import("root.zig").Context;
543     var ctx = try Context.init(testing.allocator, Context.Limits.testing);
544     defer ctx.deinit(testing.allocator);
545     try ctx.allowUnregistered();
546 
547     const int_attr = try ctx.getI64Attr(42);
548     const string_attr = try ctx.getStringAttr("name");
549     const dialect_attr = try ctx.getDialectAttr("test.flag", "payload");
550 
551     try testing.expect(int_attr.hasStorageType(Attribute.IntegerAttr));
552     try testing.expect(int_attr.cast(Attribute.IntegerAttr) != null);
553     try testing.expect(int_attr.cast(Attribute.StringAttr) == null);
554     try testing.expect(int_attr.cast(Attribute.DialectAttr) == null);
555 
556     try testing.expect(string_attr.cast(Attribute.StringAttr) != null);
557     try testing.expect(string_attr.cast(Attribute.IntegerAttr) == null);
558 
559     try testing.expect(dialect_attr.hasStorageType(Attribute.DialectAttr));
560     try testing.expect(dialect_attr.cast(Attribute.DialectAttr) != null);
561     try testing.expect(dialect_attr.cast(Attribute.BoolAttr) == null);
562 }
563 
564 test "Context getIntegerAttr creates typed integer attribute" {
565     const testing = std.testing;
566     const Context = @import("root.zig").Context;
567     var ctx = try Context.init(testing.allocator, Context.Limits.testing);
568     defer ctx.deinit(testing.allocator);
569 
570     const attr = try ctx.getI64Attr(42);
571 
572     try testing.expectEqualStrings(attribute.builtin_attr_names.integer, attr.abstract.name);
573 
574     const int_attr = attr.cast(Attribute.IntegerAttr).?;
575     try testing.expectEqual(@as(i64, 42), int_attr.getValue());
576     try testing.expectEqual(@as(u8, 64), int_attr.width);
577     try testing.expect(int_attr.is_signed);
578 }
579 
580 test "Context getIntegerAttr uniquing works correctly" {
581     const testing = std.testing;
582     const Context = @import("root.zig").Context;
583     var ctx = try Context.init(testing.allocator, Context.Limits.testing);
584     defer ctx.deinit(testing.allocator);
585 
586     const attr1 = try ctx.getI64Attr(42);
587     const attr2 = try ctx.getI64Attr(42);
588     const attr3 = try ctx.getI64Attr(100);
589 
590     try testing.expectEqual(attr1.impl, attr2.impl);
591 
592     try testing.expect(attr1.impl != attr3.impl);
593 }
594 
595 test "Context getFloatAttr creates typed float attribute" {
596     const testing = std.testing;
597     const Context = @import("root.zig").Context;
598     var ctx = try Context.init(testing.allocator, Context.Limits.testing);
599     defer ctx.deinit(testing.allocator);
600 
601     const attr = try ctx.getF64Attr(3.14);
602 
603     try testing.expectEqualStrings(attribute.builtin_attr_names.float_, attr.abstract.name);
604 
605     const float_attr = attr.cast(Attribute.FloatAttr).?;
606     try testing.expectApproxEqAbs(@as(f64, 3.14), float_attr.getValue(), 0.001);
607     try testing.expectEqual(@as(u8, 64), float_attr.width);
608 }
609 
610 test "Context getFloatAttr uniquing handles special values" {
611     const testing = std.testing;
612     const Context = @import("root.zig").Context;
613     var ctx = try Context.init(testing.allocator, Context.Limits.testing);
614     defer ctx.deinit(testing.allocator);
615 
616     const pos_zero = try ctx.getF64Attr(0.0);
617     const neg_zero = try ctx.getF64Attr(-0.0);
618 
619     try testing.expect(pos_zero.impl != neg_zero.impl);
620 }
621 
622 test "Context getBoolAttr creates typed bool attribute" {
623     const testing = std.testing;
624     const Context = @import("root.zig").Context;
625     var ctx = try Context.init(testing.allocator, Context.Limits.testing);
626     defer ctx.deinit(testing.allocator);
627 
628     const true_attr = try ctx.getBoolAttr(true);
629     const false_attr = try ctx.getBoolAttr(false);
630 
631     try testing.expectEqualStrings(attribute.builtin_attr_names.bool_, true_attr.abstract.name);
632     try testing.expectEqualStrings(attribute.builtin_attr_names.bool_, false_attr.abstract.name);
633 
634     const true_val = true_attr.cast(Attribute.BoolAttr).?;
635     const false_val = false_attr.cast(Attribute.BoolAttr).?;
636     try testing.expect(true_val.getValue());
637     try testing.expect(!false_val.getValue());
638 }
639 
640 test "Context getStringAttr creates typed string attribute" {
641     const testing = std.testing;
642     const Context = @import("root.zig").Context;
643     var ctx = try Context.init(testing.allocator, Context.Limits.testing);
644     defer ctx.deinit(testing.allocator);
645 
646     const attr = try ctx.getStringAttr("hello");
647 
648     try testing.expectEqualStrings(attribute.builtin_attr_names.string, attr.abstract.name);
649 
650     const str_attr = attr.cast(Attribute.StringAttr).?;
651     try testing.expectEqualStrings("hello", str_attr.getValue());
652 }
653 
654 test "Context getStringAttr uniquing works correctly" {
655     const testing = std.testing;
656     const Context = @import("root.zig").Context;
657     var ctx = try Context.init(testing.allocator, Context.Limits.testing);
658     defer ctx.deinit(testing.allocator);
659 
660     const attr1 = try ctx.getStringAttr("test");
661     const attr2 = try ctx.getStringAttr("test");
662     const attr3 = try ctx.getStringAttr("different");
663 
664     try testing.expectEqual(attr1.impl, attr2.impl);
665 
666     try testing.expect(attr1.impl != attr3.impl);
667 }
668 
669 test "Context getSymbolRefAttr creates and uniques symbol references" {
670     const testing = std.testing;
671     const Context = @import("root.zig").Context;
672     var ctx = try Context.init(testing.allocator, Context.Limits.testing);
673     defer ctx.deinit(testing.allocator);
674 
675     const attr1 = try ctx.getSymbolRefAttr("module", &.{ "nested", "leaf" });
676     const attr2 = try ctx.getSymbolRefAttr("module", &.{ "nested", "leaf" });
677     const attr3 = try ctx.getSymbolRefAttr("module", &.{"other"});
678     const flat = try ctx.getFlatSymbolRefAttr("module");
679 
680     try testing.expectEqual(attr1.impl, attr2.impl);
681     try testing.expect(attr1.impl != attr3.impl);
682     try testing.expect(flat.impl != attr1.impl);
683     try testing.expectEqualStrings(attribute.builtin_attr_names.symbol_ref, attr1.abstract.name);
684 
685     const ref_attr = attr1.cast(Attribute.SymbolRefAttr).?;
686     try testing.expectEqualStrings("module", ref_attr.getRootReference());
687     try testing.expectEqualStrings("leaf", ref_attr.getLeafReference());
688     try testing.expect(!ref_attr.isFlat());
689 
690     const rendered = try std.fmt.allocPrint(testing.allocator, "{f}", .{attr1});
691     defer testing.allocator.free(rendered);
692     try testing.expectEqualStrings("@module::@nested::@leaf", rendered);
693 }
694 
695 test "Context getStringListAttr creates and uniques structural string lists" {
696     const testing = std.testing;
697     const Context = @import("root.zig").Context;
698     var ctx = try Context.init(testing.allocator, Context.Limits.testing);
699     defer ctx.deinit(testing.allocator);
700 
701     const attr1 = try ctx.getStringListAttr(&.{ "ctx", "value", "result" });
702     const attr2 = try ctx.getStringListAttr(&.{ "ctx", "value", "result" });
703     const attr3 = try ctx.getStringListAttr(&.{ "ctx", "result" });
704 
705     try testing.expectEqualStrings(attribute.builtin_attr_names.string_list, attr1.abstract.name);
706     try testing.expectEqual(attr1.impl, attr2.impl);
707     try testing.expect(attr1.impl != attr3.impl);
708 
709     const list_attr = attr1.cast(Attribute.StringListAttr).?;
710     const values = list_attr.getValues();
711     try testing.expectEqual(@as(usize, 3), values.len);
712     try testing.expectEqualStrings("ctx", values[0]);
713     try testing.expectEqualStrings("value", values[1]);
714     try testing.expectEqualStrings("result", values[2]);
715 }
716 
717 test "Context getTypeListAttr creates and uniques structural type lists" {
718     const testing = std.testing;
719     const Context = @import("root.zig").Context;
720     var ctx = try Context.init(testing.allocator, Context.Limits.testing);
721     defer ctx.deinit(testing.allocator);
722     try ctx.allowUnregistered();
723 
724     const i32_type = try ctx.getDialectTypeFromName("arith.i32");
725     const i64_type = try ctx.getDialectTypeFromName("arith.i64");
726 
727     const attr1 = try ctx.getTypeListAttr(&.{ i32_type, i64_type });
728     const attr2 = try ctx.getTypeListAttr(&.{ i32_type, i64_type });
729     const attr3 = try ctx.getTypeListAttr(&.{i64_type});
730 
731     try testing.expectEqualStrings(attribute.builtin_attr_names.type_list, attr1.abstract.name);
732     try testing.expectEqual(attr1.impl, attr2.impl);
733     try testing.expect(attr1.impl != attr3.impl);
734 
735     const list_attr = attr1.cast(Attribute.TypeListAttr).?;
736     const values = list_attr.getValues();
737     try testing.expectEqual(@as(usize, 2), values.len);
738     try testing.expect(values[0].eql(i32_type));
739     try testing.expect(values[1].eql(i64_type));
740 }
741 
742 test "Context getArrayAttr creates and uniques structural attribute arrays" {
743     const testing = std.testing;
744     const Context = @import("root.zig").Context;
745     var ctx = try Context.init(testing.allocator, Context.Limits.testing);
746     defer ctx.deinit(testing.allocator);
747 
748     const name_attr = try ctx.getStringAttr("name");
749     const value_attr = try ctx.getI64Attr(7);
750     const attr1 = try ctx.getArrayAttr(&.{ name_attr, value_attr });
751     const attr2 = try ctx.getArrayAttr(&.{ name_attr, value_attr });
752     const attr3 = try ctx.getArrayAttr(&.{ value_attr, name_attr });
753 
754     try testing.expectEqualStrings(attribute.builtin_attr_names.array, attr1.abstract.name);
755     try testing.expectEqual(attr1.impl, attr2.impl);
756     try testing.expect(attr1.impl != attr3.impl);
757 
758     const array_attr = attr1.cast(Attribute.ArrayAttr).?;
759     const values = array_attr.getValues();
760     try testing.expectEqual(@as(usize, 2), values.len);
761     try testing.expect(values[0].eql(name_attr));
762     try testing.expect(values[1].eql(value_attr));
763 
764     const iface = attr1.interface(interfaces.AttributeArrayInterface).?;
765     try testing.expectEqual(@as(usize, 2), iface.call(.getCount, .{}));
766     try testing.expect((iface.call(.getElement, .{0}) orelse return error.TestExpectedAttribute).eql(name_attr));
767     try testing.expect((iface.call(.getElement, .{1}) orelse return error.TestExpectedAttribute).eql(value_attr));
768     try testing.expect(iface.call(.getElement, .{2}) == null);
769 }
770 
771 test "attribute interning rolls back retained payloads on named table exhaustion" {
772     comptime {
773         @stardustClaim(
774             @import("alloc_phase").capacity.witness(@import("./root.zig").Context, "choir_context_attribute_exhaustion"),
775             null,
776             null,
777             null,
778             null,
779             null,
780             null,
781         );
782     }
783 
784     const testing = std.testing;
785     const Context = @import("root.zig").Context;
786     const values = &.{ "first", "second", "third" };
787     const Helpers = struct {
788         fn prepare(ctx: *Context) !void {
789             const iface = interfaces.AttributeEqualInterface.entry(
790                 &attribute.string_list_attr_eql_vtable,
791             );
792             _ = try ctx.registerAttributeType(
793                 attribute.builtin_attr_names.string_list,
794                 &.{iface},
795             );
796         }
797     };
798 
799     var table_frontier: usize = undefined;
800     var table_delta: usize = undefined;
801     {
802         var probe = try Context.init(testing.allocator, Context.Limits.testing);
803         defer probe.deinit(testing.allocator);
804         try Helpers.prepare(&probe);
805         table_frontier = probe.capacityUsage().attribute_tables.frontier_bytes;
806         _ = try probe.getStringListAttr(values);
807         table_delta = probe.capacityUsage().attribute_tables.frontier_bytes - table_frontier;
808         try testing.expect(table_delta > 0);
809     }
810 
811     var limits = Context.Limits.testing;
812     limits.attributes.table_bytes = table_frontier + table_delta - 1;
813     var context = try Context.init(testing.allocator, limits);
814     defer context.deinit(testing.allocator);
815     try Helpers.prepare(&context);
816     try testing.expectEqual(
817         table_frontier,
818         context.capacityUsage().attribute_tables.frontier_bytes,
819     );
820     context.activate();
821 
822     const payload_frontier = context.capacityUsage().attribute_payloads.frontier_bytes;
823     try testing.expectError(error.OutOfMemory, context.getStringListAttr(values));
824     try testing.expectEqual(
825         payload_frontier,
826         context.capacityUsage().attribute_payloads.frontier_bytes,
827     );
828     try testing.expect(context.attr_interner.storage.string_list_attrs.get(.{
829         .values = values,
830     }) == null);
831     try testing.expectEqual(
832         Context.Segment.attribute_tables,
833         context.exhaustedSegment().?,
834     );
835 }
836 
837 test "Typed attribute equality via interface" {
838     const testing = std.testing;
839     const Context = @import("root.zig").Context;
840     var ctx = try Context.init(testing.allocator, Context.Limits.testing);
841     defer ctx.deinit(testing.allocator);
842     try ctx.allowUnregistered();
843 
844     const int1 = try ctx.getI64Attr(42);
845     const int2 = try ctx.getI64Attr(42);
846     const int3 = try ctx.getI64Attr(100);
847     try testing.expect(int1.eql(int2));
848     try testing.expect(!int1.eql(int3));
849 
850     const f1 = try ctx.getF64Attr(3.14);
851     const f2 = try ctx.getF64Attr(3.14);
852     const f3 = try ctx.getF64Attr(2.71);
853     try testing.expect(f1.eql(f2));
854     try testing.expect(!f1.eql(f3));
855 
856     const b1 = try ctx.getBoolAttr(true);
857     const b2 = try ctx.getBoolAttr(true);
858     const b3 = try ctx.getBoolAttr(false);
859     try testing.expect(b1.eql(b2));
860     try testing.expect(!b1.eql(b3));
861 
862     const s1 = try ctx.getStringAttr("hello");
863     const s2 = try ctx.getStringAttr("hello");
864     const s3 = try ctx.getStringAttr("world");
865     try testing.expect(s1.eql(s2));
866     try testing.expect(!s1.eql(s3));
867 
868     const sl1 = try ctx.getStringListAttr(&.{ "ctx", "value", "result" });
869     const sl2 = try ctx.getStringListAttr(&.{ "ctx", "value", "result" });
870     const sl3 = try ctx.getStringListAttr(&.{ "ctx", "result" });
871     try testing.expect(sl1.eql(sl2));
872     try testing.expect(!sl1.eql(sl3));
873 
874     const i32_type = try ctx.getDialectTypeFromName("arith.i32");
875     const i64_type = try ctx.getDialectTypeFromName("arith.i64");
876     const tl1 = try ctx.getTypeListAttr(&.{ i32_type, i64_type });
877     const tl2 = try ctx.getTypeListAttr(&.{ i32_type, i64_type });
878     const tl3 = try ctx.getTypeListAttr(&.{i64_type});
879     try testing.expect(tl1.eql(tl2));
880     try testing.expect(!tl1.eql(tl3));
881 }
882 
883 test "Context getFloatAttr uniquing handles NaN correctly" {
884     const testing = std.testing;
885     const Context = @import("root.zig").Context;
886     var ctx = try Context.init(testing.allocator, Context.Limits.testing);
887     defer ctx.deinit(testing.allocator);
888 
889     const nan = std.math.nan(f64);
890 
891     const nan1 = try ctx.getF64Attr(nan);
892     const nan2 = try ctx.getF64Attr(nan);
893 
894     try testing.expectEqual(nan1.impl, nan2.impl);
895 
896     const float_attr = nan1.cast(Attribute.FloatAttr).?;
897     try testing.expect(std.math.isNan(float_attr.getValue()));
898 }