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 }