lib/choir/src/backends/x64/data.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 //! Data-symbol attributes on x86_64 integer constants, and the symbols a `memref.global`
  2 //! declares.
  3 //!
  4 //! A constant that carries them evaluates to the address of its read-only bytes, not to its
  5 //! integer value. A `memref.global` marked `constant` is the same thing spelled as a
  6 //! declaration, so both arrive here and both leave as a `machine.DataSymbol`.
  7 
  8 const std = @import("std");
  9 const alloc_arena = @import("alloc_arena");
 10 const ir = @import("../../core/root.zig");
 11 const dialects = @import("../../dialects/root.zig");
 12 const machine = @import("../root.zig").machine_code;
 13 const memref = @import("../../dialects/memref.zig");
 14 const encoding = @import("encoding.zig");
 15 
 16 const names = machine.data_symbol_attr_names;
 17 const ConstantOp = dialects.ArithDialect.ConstantOp;
 18 const GlobalOp = memref.MemrefDialect.GlobalOp;
 19 
 20 pub const Error = error{InvalidDataSymbol};
 21 
 22 comptime {
 23     if (memref.max_global_alignment != machine.max_data_alignment) {
 24         @compileError("memref global alignment bound must match the data symbol bound");
 25     }
 26 }
 27 
 28 /// The data symbol that a `memref.global` declares.
 29 ///
 30 /// The three placements the dialect distinguishes are the three sections a symbol can land in,
 31 /// and this is the only place that mapping is written. A zeroed global becomes a symbol with a
 32 /// stated size and no bytes rather than an empty one.
 33 pub fn symbolOfGlobal(global: GlobalOp) Error!machine.DataSymbol {
 34     const name = global.getSymName() orelse return error.InvalidDataSymbol;
 35     const placement = global.getPlacement() orelse return error.InvalidDataSymbol;
 36     const memref_type = global.getType() orelse return error.InvalidDataSymbol;
 37     const size = memref.staticByteSize(memref_type) orelse return error.InvalidDataSymbol;
 38     const alignment = global.getAlignment() orelse return error.InvalidDataSymbol;
 39     if (alignment > machine.max_data_alignment) return error.InvalidDataSymbol;
 40     if (size > std.math.maxInt(usize)) return error.InvalidDataSymbol;
 41     const symbol: machine.DataSymbol = switch (placement) {
 42         .read_only, .writable => .{
 43             .name = name,
 44             .bytes = global.getInitial() orelse return error.InvalidDataSymbol,
 45             .alignment = @intCast(alignment),
 46             .binding = .global,
 47             .section = if (placement == .read_only) .rodata else .data,
 48         },
 49         .zeroed => .{
 50             .name = name,
 51             .reserved_size = @intCast(size),
 52             .alignment = @intCast(alignment),
 53             .binding = .global,
 54             .section = .bss,
 55         },
 56     };
 57     if (!symbol.isValid()) return error.InvalidDataSymbol;
 58     return symbol;
 59 }
 60 
 61 /// Adds the symbol of every `memref.global` in `module`'s body to `set`.
 62 ///
 63 /// An object path calls this because a declaration is a linker interface: the symbol belongs in
 64 /// `.symtab` whether or not code in this translation unit addresses it, and dropping an
 65 /// unreferenced one is how a module ends up charging for storage nothing emits. The JIT keeps
 66 /// the use-site rule instead, because a global no function names has no address any caller there
 67 /// could obtain. Re-adding a symbol an emitter already recorded is safe: `symbolOfGlobal` is a
 68 /// function of the declaration alone, so the two agree and `put` keeps the one entry.
 69 pub fn appendModuleGlobals(
 70     allocator: std.mem.Allocator,
 71     set: *machine.DataSymbolSet,
 72     module: *ir.Operation,
 73 ) (std.mem.Allocator.Error || Error || machine.DataSymbolError)!void {
 74     const region = module.getRegion(0) orelse return;
 75     const block = region.getEntryBlock() orelse return;
 76     var op_iter = block.operations.head;
 77     while (op_iter) |op_ptr| {
 78         const op: *ir.Operation = @ptrCast(@alignCast(op_ptr));
 79         op_iter = op.next_op;
 80         if (!std.mem.eql(u8, op.name.name, GlobalOp.operation_name)) continue;
 81         try set.put(allocator, try symbolOfGlobal(.{ .op = op }));
 82     }
 83 }
 84 
 85 /// Loads the address of `symbol` into `result` through a 64-bit immediate a data relocation
 86 /// fills, and records the symbol so the object or the JIT mapping carries it.
 87 ///
 88 /// Both an `arith.constant` carrying data attributes and a `memref.get_global` reach a symbol
 89 /// this way, so the instruction and the relocation that completes it are written once.
 90 pub fn emitAddress(self: anytype, result: *ir.Value, symbol: machine.DataSymbol) !void {
 91     std.debug.assert(symbol.isValid());
 92     const slot = try self.slotFor(result);
 93     std.debug.assert(slot.width == 64);
 94     try self.data_symbols.put(self.allocator, symbol);
 95     const mov = encoding.movRegImm64(.rax, 0);
 96     const mov_offset = self.code.items.len;
 97     try self.emitEncoding(mov);
 98     try self.data_relocations.append(self.allocator, .{
 99         .offset = mov_offset + (mov.len - 8),
100         .target = symbol.name,
101     });
102     try self.storeFrom(result, .rax);
103 }
104 
105 /// Reports whether `op` carries any data-symbol attribute, well formed or not.
106 pub fn hasAttributes(op: *ir.Operation) bool {
107     return op.getAttr(names.name) != null or
108         op.getAttr(names.bytes) != null or
109         op.getAttr(names.alignment) != null;
110 }
111 
112 /// Returns the data symbol that `op` names, or `null` when `op` carries no data-symbol attribute.
113 /// Fails unless `op` is an `arith.constant` of type `arith.i64`, `arith.u64`, or `arith.index` with
114 /// a nonempty string name, nonempty string bytes, and an optional integer alignment that is a power
115 /// of two up to `machine.max_data_alignment`.
116 pub fn symbolOf(op: *ir.Operation) Error!?machine.DataSymbol {
117     if (!hasAttributes(op)) return null;
118     if (!isAddressConstant(op)) return error.InvalidDataSymbol;
119     const name = op.getAttrAs(ir.Attribute.StringAttr, names.name) orelse
120         return error.InvalidDataSymbol;
121     const bytes = op.getAttrAs(ir.Attribute.StringAttr, names.bytes) orelse
122         return error.InvalidDataSymbol;
123     const symbol = machine.DataSymbol{
124         .name = name.getValue(),
125         .bytes = bytes.getValue(),
126         .alignment = try alignmentOf(op),
127     };
128     if (!symbol.isValid()) return error.InvalidDataSymbol;
129     return symbol;
130 }
131 
132 fn alignmentOf(op: *ir.Operation) Error!usize {
133     if (op.getAttr(names.alignment) == null) return 1;
134     const attr = op.getAttrAs(ir.Attribute.IntegerAttr, names.alignment) orelse
135         return error.InvalidDataSymbol;
136     const value = attr.getValue();
137     if (value < 1 or value > machine.max_data_alignment) return error.InvalidDataSymbol;
138     return @intCast(value);
139 }
140 
141 fn isAddressConstant(op: *ir.Operation) bool {
142     if (!std.mem.eql(u8, op.name.name, ConstantOp.operation_name)) return false;
143     if (op.results.items.len != 1) return false;
144     const type_name = op.results.items[0].type.getDialectTypeName() orelse return false;
145     return std.mem.eql(u8, type_name, "arith.i64") or
146         std.mem.eql(u8, type_name, "arith.u64") or
147         std.mem.eql(u8, type_name, "arith.index");
148 }
149 
150 const Attributes = struct {
151     name: ?[]const u8 = "table",
152     bytes: ?[]const u8 = "\x01\x02\x03",
153     alignment: ?i64 = null,
154 };
155 
156 fn attach(ctx: *ir.Context, op: *ir.Operation, attributes: Attributes) !*ir.Operation {
157     if (attributes.name) |value| try op.setAttr(names.name, try ctx.getStringAttr(value));
158     if (attributes.bytes) |value| try op.setAttr(names.bytes, try ctx.getStringAttr(value));
159     if (attributes.alignment) |value| {
160         try op.setAttr(names.alignment, try ctx.getI64Attr(value));
161     }
162     return op;
163 }
164 
165 test "x86_64 data symbols read only well-formed constant attributes" {
166     var arena = alloc_arena.Arena.init(std.testing.allocator);
167     defer arena.deinit();
168     const allocator = arena.allocator();
169     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
170     defer ctx.deinit(allocator);
171     try dialects.registerAllDialects(&ctx);
172     const Arith = dialects.ArithDialect;
173     const loc = ir.Location.getFile("data.choir", 2, 7);
174     const i64_type = try Arith.getScalarType(&ctx, .i64);
175 
176     const page = Attributes{ .alignment = machine.max_data_alignment };
177     const aligned = try attach(&ctx, (try ConstantOp.createInt(&ctx, loc, i64_type, 7)).op, page);
178     const symbol = (try symbolOf(aligned)).?;
179     try std.testing.expectEqualStrings("table", symbol.name);
180     try std.testing.expectEqualStrings("\x01\x02\x03", symbol.bytes);
181     try std.testing.expectEqual(@as(usize, machine.max_data_alignment), symbol.alignment);
182     const index_type = try Arith.getIndexType(&ctx);
183     const index = try attach(&ctx, (try ConstantOp.createInt(&ctx, loc, index_type, 0)).op, .{});
184     try std.testing.expectEqual(@as(usize, 1), (try symbolOf(index)).?.alignment);
185     const plain = (try ConstantOp.createInt(&ctx, loc, i64_type, 7)).op;
186     try std.testing.expect(!hasAttributes(plain));
187     try std.testing.expect((try symbolOf(plain)) == null);
188 
189     const malformed = [_]Attributes{
190         .{ .name = null },
191         .{ .bytes = null },
192         .{ .name = "" },
193         .{ .bytes = "" },
194         .{ .name = null, .bytes = null, .alignment = 8 },
195         .{ .alignment = 0 },
196         .{ .alignment = -8 },
197         .{ .alignment = 3 },
198         .{ .alignment = 2 * machine.max_data_alignment },
199     };
200     for (malformed) |attributes| {
201         const constant = try ConstantOp.createInt(&ctx, loc, i64_type, 7);
202         const op = try attach(&ctx, constant.op, attributes);
203         try std.testing.expect(hasAttributes(op));
204         try std.testing.expectError(error.InvalidDataSymbol, symbolOf(op));
205     }
206     const i32_type = try Arith.getScalarType(&ctx, .i32);
207     const narrow = try attach(&ctx, (try ConstantOp.createInt(&ctx, loc, i32_type, 7)).op, .{});
208     try std.testing.expectError(error.InvalidDataSymbol, symbolOf(narrow));
209     const flag = try attach(&ctx, (try ConstantOp.createBool(&ctx, loc, true)).op, .{});
210     try std.testing.expectError(error.InvalidDataSymbol, symbolOf(flag));
211 }