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 }