lib/choir/src/core/parse.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const alloc_arena = @import("alloc_arena");
  3 
  4 const Attribute = @import("attribute.zig").Attribute;
  5 const context_mod = @import("context/root.zig");
  6 const Context = context_mod.Context;
  7 const Location = @import("location.zig").Location;
  8 const NamedAttribute = @import("attribute.zig").NamedAttribute;
  9 const Operation = @import("operation/root.zig").Operation;
 10 const Region = @import("region.zig").Region;
 11 const Type = @import("type.zig").Type;
 12 const Value = @import("value.zig").Value;
 13 const dump = @import("dump.zig");
 14 const interfaces = @import("interfaces/root.zig");
 15 
 16 pub const ParseError = error{
 17     DuplicateValueId,
 18     ExpectedAttributeName,
 19     ExpectedBlock,
 20     ExpectedByte,
 21     ExpectedColon,
 22     ExpectedComma,
 23     ExpectedEquals,
 24     ExpectedHeaderTerminator,
 25     ExpectedListEnd,
 26     ExpectedNewline,
 27     ExpectedOperationName,
 28     ExpectedRegion,
 29     ExpectedString,
 30     ExpectedType,
 31     ExpectedValue,
 32     InvalidAttribute,
 33     InvalidBool,
 34     InvalidFloat,
 35     InvalidInteger,
 36     InvalidType,
 37     InvalidValue,
 38     MissingValue,
 39     TrailingInput,
 40     UnexpectedEnd,
 41     UnsupportedSuccessors,
 42 };
 43 
 44 const ParseTarget = union(enum) {
 45     operation: void,
 46     region: void,
 47     block: *Region,
 48 };
 49 
 50 const ParseResult = union(enum) {
 51     operation: *Operation,
 52     region: *Region,
 53     block: void,
 54 };
 55 
 56 pub const Source = struct {
 57     name: []const u8,
 58     text: []const u8,
 59 };
 60 
 61 pub fn operation(ctx: *Context, text: []const u8) anyerror!*Operation {
 62     return source(ctx, .{ .name = "<string>", .text = text });
 63 }
 64 
 65 /// Parses named text with Context-owned filenames and half-open byte ranges.
 66 pub fn source(ctx: *Context, input: Source) anyerror!*Operation {
 67     var parser = try Parser.init(ctx, input);
 68     defer parser.deinit();
 69     const op = try parser.parseOperation();
 70     parser.skipBlankLines();
 71     if (!parser.atEnd()) return ParseError.TrailingInput;
 72     return op;
 73 }
 74 
 75 const Parser = struct {
 76     ctx: *Context,
 77     allocator: std.mem.Allocator,
 78     operation_allocator: std.mem.Allocator,
 79     text: []const u8,
 80     source_name: []const u8,
 81     line_starts: std.ArrayList(usize),
 82     index: usize = 0,
 83     values: std.AutoHashMap(usize, *Value),
 84 
 85     fn init(ctx: *Context, input: Source) !Parser {
 86         const transient_allocator = context_mod.transientAllocator(ctx);
 87         const operation_allocator = context_mod.operationAllocator(ctx);
 88         var line_starts: std.ArrayList(usize) = .empty;
 89         errdefer line_starts.deinit(transient_allocator);
 90         try line_starts.append(transient_allocator, 0);
 91         for (input.text, 0..) |byte, index| {
 92             if (byte == '\n') try line_starts.append(transient_allocator, index + 1);
 93         }
 94         const source_name = try operation_allocator.dupe(u8, input.name);
 95         return .{
 96             .ctx = ctx,
 97             .allocator = transient_allocator,
 98             .operation_allocator = operation_allocator,
 99             .text = input.text,
100             .source_name = source_name,
101             .line_starts = line_starts,
102             .values = std.AutoHashMap(usize, *Value).init(transient_allocator),
103         };
104     }
105 
106     fn deinit(self: *Parser) void {
107         self.values.deinit();
108         self.line_starts.deinit(self.allocator);
109     }
110 
111     fn position(self: *const Parser, byte: usize) Location.FilePosition {
112         std.debug.assert(byte <= self.text.len);
113         var low: usize = 0;
114         var high = self.line_starts.items.len;
115         while (low + 1 < high) {
116             const middle = low + (high - low) / 2;
117             if (self.line_starts.items[middle] <= byte) {
118                 low = middle;
119             } else {
120                 high = middle;
121             }
122         }
123         return .{
124             .byte = byte,
125             .line = @intCast(low + 1),
126             .column = @intCast(byte - self.line_starts.items[low] + 1),
127         };
128     }
129 
130     fn location(self: *const Parser, start: usize, end: usize) Location {
131         return Location.getFileRange(self.source_name, self.position(start), self.position(end));
132     }
133 
134     fn parseOperation(self: *Parser) anyerror!*Operation {
135         const parsed = try self.parseStructure(.{ .operation = {} });
136         return switch (parsed) {
137             .operation => |op| op,
138             else => unreachable,
139         };
140     }
141 
142     fn parseStructure(self: *Parser, target: ParseTarget) anyerror!ParseResult {
143         switch (target) {
144             .operation => {
145                 self.skipSpaces();
146                 const header_start = self.index;
147                 const header_length = std.mem.indexOfAny(u8, self.text[header_start..], "\r\n") orelse
148                     self.text.len - header_start;
149                 const loc = self.location(header_start, header_start + header_length);
150 
151                 var result_ids: std.ArrayListUnmanaged(usize) = .empty;
152                 defer result_ids.deinit(self.allocator);
153                 try self.parseResultIds(&result_ids);
154 
155                 const op_name = try self.parseOperationName();
156                 try self.expectByte('(');
157 
158                 var operands: std.ArrayListUnmanaged(*Value) = .empty;
159                 defer operands.deinit(self.allocator);
160                 try self.parseOperands(&operands);
161                 try self.expectByte(')');
162 
163                 self.skipSpaces();
164                 if (self.consumeArrow()) return ParseError.UnsupportedSuccessors;
165 
166                 var properties: ?Attribute = null;
167                 if (self.consumeLiteral("properties(")) {
168                     properties = try self.parseAttribute();
169                     self.skipSpaces();
170                     try self.expectByte(')');
171                 }
172 
173                 self.skipSpaces();
174                 var attrs: std.ArrayListUnmanaged(NamedAttribute) = .empty;
175                 defer attrs.deinit(self.allocator);
176                 if (self.peekByte() == '{' and !self.peekRegionStart()) {
177                     try self.parseAttributes(&attrs);
178                 }
179 
180                 self.skipSpaces();
181                 var result_types: std.ArrayListUnmanaged(Type) = .empty;
182                 defer result_types.deinit(self.allocator);
183                 if (self.consumeByte(':')) {
184                     try self.parseTypeListUntilHeaderEnd(&result_types);
185                 }
186 
187                 if (result_ids.items.len != result_types.items.len) return ParseError.InvalidValue;
188 
189                 var regions: std.ArrayListUnmanaged(*Region) = .empty;
190                 defer {
191                     for (regions.items) |region| {
192                         region.deinit();
193                         self.operation_allocator.destroy(region);
194                     }
195                     regions.deinit(self.allocator);
196                 }
197 
198                 while (true) {
199                     self.skipSpaces();
200                     if (!self.peekRegionStart()) break;
201                     const parsed = try self.parseStructure(.{ .region = {} });
202                     const region = switch (parsed) {
203                         .region => |region| region,
204                         else => unreachable,
205                     };
206                     try regions.append(self.allocator, region);
207                 }
208 
209                 self.consumeOperationEnd();
210 
211                 var state = Operation.State.init(op_name, loc);
212                 state.addOperands(operands.items);
213                 state.addTypes(result_types.items);
214                 state.addRawAttributes(attrs.items);
215                 if (properties) |payload| try state.setPropertiesAttr(payload);
216                 state.addRegionBodies(regions.items);
217 
218                 const op = try self.ctx.createOperation(state);
219                 errdefer op.erase();
220 
221                 for (result_ids.items, 0..) |id, result_index| {
222                     try self.recordValue(id, op.getResult(result_index) orelse return ParseError.InvalidValue);
223                 }
224 
225                 return .{ .operation = op };
226             },
227             .region => {
228                 try self.expectByte('{');
229                 try self.expectNewline();
230 
231                 const region = try self.operation_allocator.create(Region);
232                 region.* = Region.init(self.operation_allocator);
233                 errdefer {
234                     region.deinit();
235                     self.operation_allocator.destroy(region);
236                 }
237 
238                 while (true) {
239                     self.skipSpaces();
240                     if (self.consumeByte('}')) break;
241                     if (self.atEnd()) return ParseError.UnexpectedEnd;
242                     _ = try self.parseStructure(.{ .block = region });
243                 }
244 
245                 return .{ .region = region };
246             },
247             .block => |region| {
248                 self.skipSpaces();
249                 try self.expectByte('^');
250                 try self.expectLiteral("bb");
251                 _ = try self.parseUnsigned();
252 
253                 const block = try region.addBlock();
254 
255                 if (self.consumeByte('(')) {
256                     self.skipSpaces();
257                     if (!self.consumeByte(')')) {
258                         while (true) {
259                             const argument_start = self.index;
260                             const id = try self.parseValueId();
261                             try self.expectByte(':');
262                             self.skipSpaces();
263                             const typ = try self.parseType();
264                             const loc = self.location(argument_start, self.index);
265                             const arg = try block.addArgument(typ, loc);
266                             try self.recordValue(id, arg);
267                             self.skipSpaces();
268                             if (self.consumeByte(')')) break;
269                             try self.expectByte(',');
270                             self.skipSpaces();
271                         }
272                     }
273                 }
274 
275                 try self.expectByte(':');
276                 try self.expectNewline();
277 
278                 while (true) {
279                     self.skipSpaces();
280                     switch (self.peekByte()) {
281                         0 => return ParseError.UnexpectedEnd,
282                         '^', '}' => return .{ .block = {} },
283                         else => {},
284                     }
285                     const parsed = try self.parseStructure(.{ .operation = {} });
286                     const op = switch (parsed) {
287                         .operation => |op| op,
288                         else => unreachable,
289                     };
290                     var inserted = false;
291                     errdefer if (!inserted) op.erase();
292                     try block.addOperation(op);
293                     inserted = true;
294                 }
295             },
296         }
297     }
298 
299     fn parseResultIds(self: *Parser, ids: *std.ArrayListUnmanaged(usize)) !void {
300         if (self.peekByte() != '%') return;
301 
302         const start = self.index;
303         while (true) {
304             try ids.append(self.allocator, try self.parseValueId());
305             self.skipSpaces();
306             if (!self.consumeByte(',')) break;
307             self.skipSpaces();
308         }
309         self.skipSpaces();
310         if (self.consumeByte('=')) {
311             self.skipSpaces();
312             return;
313         }
314 
315         ids.clearRetainingCapacity();
316         self.index = start;
317     }
318 
319     fn parseOperationName(self: *Parser) ![]const u8 {
320         self.skipSpaces();
321         const start = self.index;
322         while (!self.atEnd() and self.peekByte() != '(' and !isNewline(self.peekByte())) {
323             self.index += 1;
324         }
325         const name = std.mem.trim(u8, self.text[start..self.index], " \t\r");
326         if (name.len == 0) return ParseError.ExpectedOperationName;
327         return name;
328     }
329 
330     fn parseOperands(self: *Parser, operands: *std.ArrayListUnmanaged(*Value)) !void {
331         self.skipSpaces();
332         if (self.peekByte() == ')') return;
333         while (true) {
334             const id = try self.parseValueId();
335             const value = self.values.get(id) orelse return ParseError.MissingValue;
336             try operands.append(self.allocator, value);
337             self.skipSpaces();
338             if (!self.consumeByte(',')) break;
339             self.skipSpaces();
340         }
341     }
342 
343     fn parseAttributes(self: *Parser, attrs: *std.ArrayListUnmanaged(NamedAttribute)) !void {
344         try self.expectByte('{');
345         self.skipSpaces();
346         if (self.consumeByte('}')) return;
347 
348         while (true) {
349             const name = try self.parseAttributeName();
350             self.skipSpaces();
351             try self.expectByte('=');
352             self.skipSpaces();
353             const attr = try self.parseAttribute();
354             try attrs.append(self.allocator, .{
355                 .name = name,
356                 .value = attr,
357             });
358             self.skipSpaces();
359             if (self.consumeByte('}')) return;
360             try self.expectByte(',');
361             self.skipSpaces();
362         }
363     }
364 
365     fn parseAttributeName(self: *Parser) ![]const u8 {
366         const start = self.index;
367         while (!self.atEnd()) {
368             const byte = self.peekByte();
369             if (byte == '=' or byte == ' ' or byte == '\t' or byte == '\r' or byte == '\n') break;
370             self.index += 1;
371         }
372         const name = self.text[start..self.index];
373         if (name.len == 0) return ParseError.ExpectedAttributeName;
374         return name;
375     }
376 
377     fn parseAttribute(self: *Parser) anyerror!Attribute {
378         self.skipSpaces();
379         if (self.atEnd()) return ParseError.UnexpectedEnd;
380 
381         if (self.peekByte() == '"') {
382             const value = try self.parseStringAlloc();
383             defer self.allocator.free(value);
384             return try self.ctx.getStringAttr(value);
385         }
386 
387         if (self.consumeLiteral("true")) return try self.ctx.getBoolAttr(true);
388         if (self.consumeLiteral("false")) return try self.ctx.getBoolAttr(false);
389 
390         if (self.peekByte() == '@') return try self.parseSymbolRefAttr();
391         if (self.peekByte() == '[') {
392             try self.expectByte('[');
393             self.skipSpaces();
394             if (self.consumeByte(']')) return try self.ctx.getStringListAttr(&.{});
395             if (self.peekByte() == '!') return try self.parseTypeListAttr();
396             if (self.peekByte() == '"') return try self.parseStringListAttr();
397 
398             var attrs: std.ArrayListUnmanaged(Attribute) = .empty;
399             defer attrs.deinit(self.allocator);
400             while (true) {
401                 try attrs.append(self.allocator, try self.parseAttribute());
402                 self.skipSpaces();
403                 if (self.consumeByte(']')) break;
404                 try self.expectByte(',');
405                 self.skipSpaces();
406             }
407             return try self.ctx.getArrayAttr(attrs.items);
408         }
409         if (self.peekByte() == '#') return try self.parseDialectAttr();
410         return try self.parseNumericAttr();
411     }
412 
413     fn parseSymbolRefAttr(self: *Parser) !Attribute {
414         try self.expectByte('@');
415         const root = try self.parseSymbolSegment();
416         var nested: std.ArrayListUnmanaged([]const u8) = .empty;
417         defer nested.deinit(self.allocator);
418 
419         while (self.consumeLiteral("::")) {
420             try self.expectByte('@');
421             try nested.append(self.allocator, try self.parseSymbolSegment());
422         }
423 
424         return try self.ctx.getSymbolRefAttr(root, nested.items);
425     }
426 
427     fn parseSymbolSegment(self: *Parser) ![]const u8 {
428         const start = self.index;
429         while (!self.atEnd()) {
430             const byte = self.peekByte();
431             if (byte == ':' or byte == ',' or byte == '}' or byte == ']' or byte == ')' or byte == ' ' or byte == '\t' or byte == '\r' or byte == '\n') break;
432             self.index += 1;
433         }
434         const segment = self.text[start..self.index];
435         if (segment.len == 0) return ParseError.InvalidAttribute;
436         return segment;
437     }
438 
439     fn parseTypeListAttr(self: *Parser) anyerror!Attribute {
440         var types: std.ArrayListUnmanaged(Type) = .empty;
441         defer types.deinit(self.allocator);
442         while (true) {
443             try types.append(self.allocator, try self.parseType());
444             self.skipSpaces();
445             if (self.consumeByte(']')) break;
446             try self.expectByte(',');
447             self.skipSpaces();
448         }
449         return try self.ctx.getTypeListAttr(types.items);
450     }
451 
452     fn parseStringListAttr(self: *Parser) anyerror!Attribute {
453         var strings: std.ArrayListUnmanaged([]u8) = .empty;
454         defer {
455             for (strings.items) |value| self.allocator.free(value);
456             strings.deinit(self.allocator);
457         }
458         while (true) {
459             try strings.append(self.allocator, try self.parseStringAlloc());
460             self.skipSpaces();
461             if (self.consumeByte(']')) break;
462             try self.expectByte(',');
463             self.skipSpaces();
464         }
465         return try self.ctx.getStringListAttr(strings.items);
466     }
467 
468     fn parseDialectAttr(self: *Parser) !Attribute {
469         try self.expectLiteral("#attr<");
470         const name_start = self.index;
471         while (!self.atEnd() and self.peekByte() != '>') self.index += 1;
472         if (self.atEnd()) return ParseError.InvalidAttribute;
473         const name = self.text[name_start..self.index];
474         try self.expectByte('>');
475 
476         if (!self.consumeByte('(')) return try self.ctx.getDialectAttr(name, "");
477         const payload = try self.parseStringAlloc();
478         defer self.allocator.free(payload);
479         try self.expectByte(')');
480         return try self.ctx.getDialectAttr(name, payload);
481     }
482 
483     fn parseNumericAttr(self: *Parser) !Attribute {
484         const number_start = self.index;
485         if (self.peekByte() == '-') self.index += 1;
486         while (!self.atEnd() and isDigit(self.peekByte())) self.index += 1;
487         if (self.peekByte() == '.') {
488             self.index += 1;
489             while (!self.atEnd() and isDigit(self.peekByte())) self.index += 1;
490         }
491         if (number_start == self.index) return ParseError.InvalidAttribute;
492         const number = self.text[number_start..self.index];
493 
494         try self.expectByte(':');
495         if (self.consumeByte('i')) {
496             const width = try self.parseUnsigned();
497             const value = std.fmt.parseInt(i64, number, 10) catch return ParseError.InvalidInteger;
498             return try self.ctx.getIntegerAttr(value, @intCast(width), true);
499         }
500         if (self.consumeByte('f')) {
501             const width = try self.parseUnsigned();
502             const value = std.fmt.parseFloat(f64, number) catch return ParseError.InvalidFloat;
503             return try self.ctx.getFloatAttr(value, @intCast(width));
504         }
505         return ParseError.InvalidAttribute;
506     }
507 
508     fn parseTypeListUntilHeaderEnd(self: *Parser, types: *std.ArrayListUnmanaged(Type)) !void {
509         while (true) {
510             self.skipSpaces();
511             try types.append(self.allocator, try self.parseType());
512             self.skipSpaces();
513             if (!self.consumeByte(',')) return;
514         }
515     }
516 
517     fn parseType(self: *Parser) !Type {
518         self.skipSpaces();
519         try self.expectByte('!');
520         const name_start = self.index;
521         while (!self.atEnd()) {
522             const byte = self.peekByte();
523             if (byte == '<' or byte == ',' or byte == ')' or byte == ']' or byte == '}' or byte == ' ' or byte == '\t' or byte == '\r' or byte == '\n') break;
524             self.index += 1;
525         }
526         const name = self.text[name_start..self.index];
527         if (name.len == 0) return ParseError.ExpectedType;
528 
529         var key: []const u8 = "";
530         if (self.consumeByte('<')) {
531             const key_start = self.index;
532             while (!self.atEnd() and self.peekByte() != '>') self.index += 1;
533             if (self.atEnd()) return ParseError.InvalidType;
534             key = self.text[key_start..self.index];
535             try self.expectByte('>');
536         }
537 
538         return try self.ctx.getDialectTypeFromNameWithKey(name, key);
539     }
540 
541     fn parseStringAlloc(self: *Parser) ![]u8 {
542         try self.expectByte('"');
543         var out: std.ArrayListUnmanaged(u8) = .empty;
544         errdefer out.deinit(self.allocator);
545 
546         while (true) {
547             if (self.atEnd()) return ParseError.ExpectedString;
548             const byte = self.nextByte();
549             if (byte == '"') return try out.toOwnedSlice(self.allocator);
550             if (byte != '\\') {
551                 try out.append(self.allocator, byte);
552                 continue;
553             }
554             if (self.atEnd()) return ParseError.ExpectedString;
555             const escaped = self.nextByte();
556             try out.append(self.allocator, switch (escaped) {
557                 '"' => '"',
558                 '\\' => '\\',
559                 'n' => '\n',
560                 'r' => '\r',
561                 't' => '\t',
562                 else => return ParseError.ExpectedString,
563             });
564         }
565     }
566 
567     fn parseValueId(self: *Parser) !usize {
568         try self.expectByte('%');
569         return try self.parseUnsigned();
570     }
571 
572     fn parseUnsigned(self: *Parser) !usize {
573         const start = self.index;
574         while (!self.atEnd() and isDigit(self.peekByte())) self.index += 1;
575         if (start == self.index) return ParseError.InvalidInteger;
576         return std.fmt.parseUnsigned(usize, self.text[start..self.index], 10) catch ParseError.InvalidInteger;
577     }
578 
579     fn recordValue(self: *Parser, id: usize, value: *Value) !void {
580         if (self.values.contains(id)) return ParseError.DuplicateValueId;
581         try self.values.put(id, value);
582     }
583 
584     fn consumeOperationEnd(self: *Parser) void {
585         self.skipSpaces();
586         _ = self.consumeByte('\n');
587     }
588 
589     fn consumeArrow(self: *Parser) bool {
590         const start = self.index;
591         if (!self.consumeByte('-')) return false;
592         if (self.consumeByte('>')) return true;
593         self.index = start;
594         return false;
595     }
596 
597     fn consumeByte(self: *Parser, byte: u8) bool {
598         if (self.peekByte() != byte) return false;
599         self.index += 1;
600         return true;
601     }
602 
603     fn consumeLiteral(self: *Parser, literal: []const u8) bool {
604         if (!std.mem.startsWith(u8, self.text[self.index..], literal)) return false;
605         self.index += literal.len;
606         return true;
607     }
608 
609     fn expectByte(self: *Parser, byte: u8) !void {
610         if (!self.consumeByte(byte)) return ParseError.ExpectedByte;
611     }
612 
613     fn expectLiteral(self: *Parser, literal: []const u8) !void {
614         if (!self.consumeLiteral(literal)) return ParseError.ExpectedByte;
615     }
616 
617     fn expectNewline(self: *Parser) !void {
618         if (self.consumeByte('\n')) return;
619         return ParseError.ExpectedNewline;
620     }
621 
622     fn nextByte(self: *Parser) u8 {
623         const byte = self.text[self.index];
624         self.index += 1;
625         return byte;
626     }
627 
628     fn peekByte(self: *const Parser) u8 {
629         if (self.index >= self.text.len) return 0;
630         return self.text[self.index];
631     }
632 
633     fn peekRegionStart(self: *const Parser) bool {
634         if (self.peekByte() != '{') return false;
635         const next = self.index + 1;
636         return next < self.text.len and self.text[next] == '\n';
637     }
638 
639     fn skipBlankLines(self: *Parser) void {
640         while (!self.atEnd()) {
641             self.skipSpaces();
642             if (!self.consumeByte('\n')) return;
643         }
644     }
645 
646     fn skipSpaces(self: *Parser) void {
647         while (!self.atEnd()) {
648             switch (self.peekByte()) {
649                 ' ', '\t', '\r' => self.index += 1,
650                 else => return,
651             }
652         }
653     }
654 
655     fn atEnd(self: *const Parser) bool {
656         return self.index >= self.text.len;
657     }
658 };
659 
660 fn isDigit(byte: u8) bool {
661     return byte >= '0' and byte <= '9';
662 }
663 
664 fn isNewline(byte: u8) bool {
665     return byte == '\n' or byte == '\r';
666 }
667 
668 const TextProperties = struct {
669     payload: ?Attribute = null,
670 
671     fn from(storage: *anyopaque) *@This() {
672         return @ptrCast(@alignCast(storage));
673     }
674 
675     fn fromConst(storage: *const anyopaque) *const @This() {
676         return @ptrCast(@alignCast(storage));
677     }
678 
679     fn init(storage: *anyopaque, _: std.mem.Allocator) anyerror!void {
680         from(storage).* = .{};
681     }
682 
683     fn deinit(_: *anyopaque, _: std.mem.Allocator) void {}
684 
685     fn get(_: *const Operation, storage: *const anyopaque, name: []const u8) ?Attribute {
686         const payload = fromConst(storage).payload orelse return null;
687         const values = payload.cast(Attribute.ArrayAttr).?.values;
688         if (std.mem.eql(u8, name, "left")) return values[0];
689         if (std.mem.eql(u8, name, "right")) return values[1];
690         return null;
691     }
692 
693     fn getProperties(_: *const Operation, storage: *const anyopaque) ?Attribute {
694         return fromConst(storage).payload;
695     }
696 
697     fn setProperties(_: *Operation, storage: *anyopaque, attr: Attribute) anyerror!void {
698         const array = attr.cast(Attribute.ArrayAttr) orelse return error.InvalidTestProperties;
699         if (array.values.len != 2) return error.InvalidTestProperties;
700         from(storage).payload = attr;
701     }
702 
703     fn copyProperties(dest: *anyopaque, original: *const anyopaque) anyerror!void {
704         from(dest).* = fromConst(original).*;
705     }
706 
707     const model = interfaces.OperationPropertiesModel{
708         .name = "test.text.properties",
709         .size = @sizeOf(@This()),
710         .alignment = std.mem.Alignment.fromByteUnits(@alignOf(@This())),
711         .init = init,
712         .deinit = deinit,
713         .getInherentAttr = get,
714         .getPropertiesAsAttr = getProperties,
715         .setPropertiesFromAttr = setProperties,
716         .copyProperties = copyProperties,
717     };
718 };
719 
720 fn registerTextProperties(context: *Context) !void {
721     try context.allowUnregistered();
722     _ = try context.registerOperation("test.text_properties", .{});
723     try context.registerOperationInherentAttributeNames(
724         "test.text_properties",
725         &.{ "left", "right" },
726     );
727     try context.registerOperationPropertiesModel(
728         "test.text_properties",
729         TextProperties.model,
730     );
731 }
732 
733 test "Choir parse round-trips properties and raw shadows" {
734     const testing = std.testing;
735     var arena = alloc_arena.Arena.init(testing.allocator);
736     defer arena.deinit();
737     const allocator = arena.allocator();
738 
739     var context = try Context.init(allocator, Context.Limits.testing);
740     defer context.deinit(allocator);
741     try registerTextProperties(&context);
742 
743     const left = try context.getI64Attr(11);
744     const right = try context.getI64Attr(22);
745     const payload = try context.getArrayAttr(&.{ left, right });
746     const raw_left = try context.getI64Attr(99);
747     const note = try context.getStringAttr("kept");
748     const raw_attributes = [_]NamedAttribute{
749         .{ .name = "left", .value = raw_left },
750         .{ .name = "debug.note", .value = note },
751     };
752     var state = Operation.State.init("test.text_properties", Location.getUnknown());
753     state.addRawAttributes(&raw_attributes);
754     try state.setPropertiesAttr(payload);
755     const original = try context.createOperation(state);
756 
757     const text = try dump.operationAlloc(allocator, original);
758     const parsed = try operation(&context, text);
759     defer parsed.erase();
760     const reparsed = try dump.operationAlloc(allocator, parsed);
761     try testing.expectEqualStrings(text, reparsed);
762     try testing.expect(std.mem.indexOf(u8, text, "properties([11:i64, 22:i64])") != null);
763     try testing.expectEqual(
764         @as(i64, 11),
765         parsed.getAttr("left").?.cast(Attribute.IntegerAttr).?.value,
766     );
767     try testing.expectEqual(
768         @as(i64, 22),
769         parsed.getAttr("right").?.cast(Attribute.IntegerAttr).?.value,
770     );
771     try testing.expectEqual(@as(usize, 2), parsed.getRawDictionaryAttrs().len);
772     try testing.expectEqual(
773         @as(i64, 99),
774         parsed.raw_dictionary_attrs.get("left").?.cast(Attribute.IntegerAttr).?.value,
775     );
776 }
777 
778 test "Choir parse owns source names and block argument ranges" {
779     const testing = std.testing;
780     var ctx = try Context.init(testing.allocator, Context.Limits.testing);
781     defer ctx.deinit(testing.allocator);
782     try ctx.allowUnregistered();
783     var name = "arguments.choir".*;
784     const text =
785         \\test.container() {
786         \\  ^bb0(%0: !test.é, %1: !test.ty):
787         \\    test.use(%0, %1)
788         \\}
789         \\
790     ;
791     const parsed = try source(&ctx, .{ .name = &name, .text = text });
792     defer parsed.erase();
793     @memset(&name, '?');
794     const block = parsed.getRegion(0).?.getEntryBlock().?;
795     const loc = block.getArgumentLocation(1).?;
796     try testing.expect(loc == .file_range);
797     const range = loc.file_range;
798     try testing.expectEqualStrings("arguments.choir", range.filename);
799     try testing.expectEqualDeep(
800         Location.FilePosition{ .byte = 40, .line = 2, .column = 22 },
801         range.start,
802     );
803     try testing.expectEqualDeep(
804         Location.FilePosition{ .byte = 52, .line = 2, .column = 34 },
805         range.end,
806     );
807     try testing.expectEqualStrings("%1: !test.ty", text[range.start.byte..range.end.byte]);
808     try testing.expectEqualStrings("arguments.choir", parsed.location.file_range.filename);
809     var ops = block.getOperations();
810     const user = ops.next().?;
811     try testing.expectEqualStrings("arguments.choir", user.location.file_range.filename);
812 }
813 
814 test "Choir parse convenience names an unnamed source" {
815     const testing = std.testing;
816     var ctx = try Context.init(testing.allocator, Context.Limits.testing);
817     defer ctx.deinit(testing.allocator);
818     try ctx.allowUnregistered();
819     const parsed = try operation(&ctx, "  test.noop()\n");
820     defer parsed.erase();
821     try testing.expect(parsed.location == .file_range);
822     const range = parsed.location.file_range;
823     try testing.expectEqualStrings("<string>", range.filename);
824     try testing.expectEqualDeep(
825         Location.FilePosition{ .byte = 2, .line = 1, .column = 3 },
826         range.start,
827     );
828     try testing.expectEqualDeep(
829         Location.FilePosition{ .byte = 13, .line = 1, .column = 14 },
830         range.end,
831     );
832 }