lib/choir/src/backends/interface.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const ir = @import("../core/root.zig");
3
4 const Allocator = std.mem.Allocator;
5
6 pub const BackendError = error{
7 UnsupportedArchitecture,
8 UnsupportedOperation,
9 NoCompiledModule,
10 FunctionNotFound,
11 CodeGenFailed,
12 JitCompileFailed,
13 JitRuntimeFull,
14 ExecutionFailed,
15 VerificationFailed,
16 OutOfMemory,
17 };
18
19 pub const max_vector_lanes: usize = 16;
20
21 pub const VectorResult = struct {
22 elem_type_name: []const u8,
23 lane_count: u8,
24 lanes: [max_vector_lanes]u64,
25 };
26
27 pub const MemrefResult = struct {
28 ptr: usize,
29 elem_type_name: []const u8,
30 len: ?u64,
31 addr_space_tag: u8,
32 };
33
34 pub const ExecuteResult = union(enum) {
35 int: i64,
36 float: f64,
37 vector: VectorResult,
38 memref: MemrefResult,
39 void_: void,
40
41 pub fn asInt(self: ExecuteResult) ?i64 {
42 return switch (self) {
43 .int => |v| v,
44 else => null,
45 };
46 }
47
48 pub fn asFloat(self: ExecuteResult) ?f64 {
49 return switch (self) {
50 .float => |v| v,
51 else => null,
52 };
53 }
54 };
55
56 pub const BackendTarget = struct {
57 name: []const u8,
58 kind: Kind,
59
60 pub const Kind = enum {
61 cpu,
62 module,
63 external,
64 };
65
66 pub const aarch64 = BackendTarget{ .name = "aarch64", .kind = .cpu };
67 pub const x86_64 = BackendTarget{ .name = "x86_64", .kind = .cpu };
68 pub const wasm = BackendTarget{ .name = "wasm", .kind = .module };
69
70 pub fn external(name: []const u8) BackendTarget {
71 return .{ .name = name, .kind = .external };
72 }
73
74 pub fn eql(self: BackendTarget, other: BackendTarget) bool {
75 return self.kind == other.kind and std.mem.eql(u8, self.name, other.name);
76 }
77 };
78
79 pub const BackendCapabilities = struct {
80 module: ModuleCapabilities = .{},
81 artifact: ArtifactCapabilities = .{},
82 cpu: CpuCapabilities = .{},
83
84 pub const ModuleCapabilities = struct {
85 verify: bool = true,
86 lower: bool = true,
87 };
88
89 pub const ArtifactCapabilities = struct {
90 machine_code: bool = false,
91 object_file: bool = false,
92 webassembly_module: bool = false,
93 source_text: bool = false,
94 };
95
96 pub const CpuCapabilities = struct {
97 disassemble: bool = false,
98 };
99 };
100
101 pub const LowerOptions = struct {
102 verify: bool = true,
103 };
104
105 pub const EmitOptions = struct {
106 entry: ?[]const u8 = null,
107 };
108
109 pub const LifecycleVTable = struct {
110 deinit: *const fn (*anyopaque, Allocator) void,
111 };
112
113 pub const ModuleVTable = struct {
114 verify: *const fn (*anyopaque, *ir.Operation) BackendError!void,
115 lower: *const fn (*anyopaque, *ir.Operation, LowerOptions) BackendError!*ir.Operation,
116 emit: *const fn (*anyopaque, *ir.Operation, EmitOptions, *std.Io.Writer) BackendError!void,
117 };
118
119 pub const DebugVTable = struct {
120 disassemble: *const fn (*anyopaque, []const u8, *std.Io.Writer) BackendError!void,
121 };
122
123 pub const BackendHandle = struct {
124 allocator: Allocator,
125 target: BackendTarget,
126 name: []const u8,
127 capabilities: BackendCapabilities,
128 ptr: *anyopaque,
129 lifecycle: *const LifecycleVTable,
130 module: *const ModuleVTable,
131 debug: ?*const DebugVTable = null,
132
133 pub fn deinit(self: *BackendHandle) void {
134 self.lifecycle.deinit(self.ptr, self.allocator);
135 }
136
137 pub fn verify(self: *BackendHandle, module: *ir.Operation) BackendError!void {
138 return self.module.verify(self.ptr, module);
139 }
140
141 pub fn lowerModule(self: *BackendHandle, module: *ir.Operation, options: LowerOptions) BackendError!*ir.Operation {
142 return self.module.lower(self.ptr, module, options);
143 }
144
145 pub fn emit(self: *BackendHandle, module: *ir.Operation, options: EmitOptions, writer: *std.Io.Writer) BackendError!void {
146 return self.module.emit(self.ptr, module, options, writer);
147 }
148
149 pub fn disassemble(self: *BackendHandle, code: []const u8, writer: *std.Io.Writer) BackendError!void {
150 const debug = self.debug orelse return BackendError.UnsupportedArchitecture;
151 return debug.disassemble(self.ptr, code, writer);
152 }
153 };
154
155 pub fn assertModuleBackend(comptime BackendType: type) void {
156 comptime {
157 if (!@hasDecl(BackendType, "init")) @compileError("backend missing init");
158 assertInitShape(BackendType);
159 if (!@hasDecl(BackendType, "deinit")) @compileError("backend missing deinit");
160 if (!@hasDecl(BackendType, "verify")) @compileError("backend missing verify");
161 if (!@hasDecl(BackendType, "lower")) @compileError("backend missing lower");
162 if (!@hasDecl(BackendType, "emit")) @compileError("backend missing emit");
163 }
164 }
165
166 pub fn assertDebugBackend(comptime BackendType: type) void {
167 comptime {
168 if (!@hasDecl(BackendType, "disassemble")) @compileError("backend missing disassemble");
169 }
170 }
171
172 pub fn initHandle(
173 comptime BackendType: type,
174 allocator: Allocator,
175 ctx: *ir.Context,
176 target: BackendTarget,
177 name: []const u8,
178 capabilities: BackendCapabilities,
179 ) BackendError!BackendHandle {
180 assertModuleBackend(BackendType);
181 const has_debug = comptime hasDebugBackend(BackendType);
182
183 if (capabilities.cpu.disassemble and !has_debug) {
184 return BackendError.UnsupportedArchitecture;
185 }
186
187 const backend_ptr = allocator.create(BackendType) catch return BackendError.OutOfMemory;
188 errdefer allocator.destroy(backend_ptr);
189 backend_ptr.* = if (comptime bounded(BackendType))
190 try BackendType.init(allocator, ctx, .standard)
191 else
192 try BackendType.init(allocator, ctx);
193
194 return .{
195 .allocator = allocator,
196 .target = target,
197 .name = name,
198 .capabilities = capabilities,
199 .ptr = backend_ptr,
200 .lifecycle = lifecycleVTableFor(BackendType),
201 .module = moduleVTableFor(BackendType),
202 .debug = if (has_debug and capabilities.cpu.disassemble) debugVTableFor(BackendType) else null,
203 };
204 }
205
206 fn hasDebugBackend(comptime BackendType: type) bool {
207 return @hasDecl(BackendType, "disassemble");
208 }
209
210 /// Whether `BackendType` bounds its own storage and so states limits when it is built. A handle
211 /// gives its caller no way to state them, so a bounded backend built this way gets `standard`.
212 fn bounded(comptime BackendType: type) bool {
213 return @hasDecl(BackendType, "Limits");
214 }
215
216 /// Asserts `init` takes what `initHandle` passes it. Checking the shape rather than the name makes
217 /// a changed constructor a compile error against this contract, instead of one inside the generic
218 /// call in `initHandle`, which names no backend a reader would recognize.
219 fn assertInitShape(comptime BackendType: type) void {
220 const params = @typeInfo(@TypeOf(BackendType.init)).@"fn".param_types;
221 if (!bounded(BackendType)) {
222 if (params.len != 2)
223 @compileError("an unbounded backend's init takes an allocator and a context");
224 return;
225 }
226 if (params.len != 3)
227 @compileError("a bounded backend's init takes an allocator, a context, and limits");
228 if (params[2] != @as(?type, BackendType.Limits))
229 @compileError("a bounded backend's init takes its Limits as the third argument");
230 }
231
232 fn lifecycleVTableFor(comptime BackendType: type) *const LifecycleVTable {
233 const VTable = struct {
234 fn deinit(ptr: *anyopaque, allocator: Allocator) void {
235 const backend: *BackendType = @ptrCast(@alignCast(ptr));
236 backend.deinit();
237 allocator.destroy(backend);
238 }
239 };
240
241 return &LifecycleVTable{ .deinit = VTable.deinit };
242 }
243
244 fn moduleVTableFor(comptime BackendType: type) *const ModuleVTable {
245 const VTable = struct {
246 fn verify(ptr: *anyopaque, module: *ir.Operation) BackendError!void {
247 const backend: *BackendType = @ptrCast(@alignCast(ptr));
248 return backend.verify(module);
249 }
250
251 fn lower(ptr: *anyopaque, module: *ir.Operation, options: LowerOptions) BackendError!*ir.Operation {
252 const backend: *BackendType = @ptrCast(@alignCast(ptr));
253 if (options.verify) {
254 try backend.verify(module);
255 }
256 return backend.lower(module);
257 }
258
259 fn emit(ptr: *anyopaque, module: *ir.Operation, options: EmitOptions, writer: *std.Io.Writer) BackendError!void {
260 const backend: *BackendType = @ptrCast(@alignCast(ptr));
261 return backend.emit(module, options, writer);
262 }
263 };
264
265 return &ModuleVTable{
266 .verify = VTable.verify,
267 .lower = VTable.lower,
268 .emit = VTable.emit,
269 };
270 }
271
272 fn debugVTableFor(comptime BackendType: type) *const DebugVTable {
273 const VTable = struct {
274 fn disassemble(ptr: *anyopaque, code: []const u8, writer: *std.Io.Writer) BackendError!void {
275 const backend: *BackendType = @ptrCast(@alignCast(ptr));
276 return backend.disassemble(code, writer);
277 }
278 };
279
280 return &DebugVTable{
281 .disassemble = VTable.disassemble,
282 };
283 }
284
285 const ModuleOnlyBackend = struct {
286 pub fn init(_: Allocator, _: *ir.Context) Allocator.Error!ModuleOnlyBackend {
287 return .{};
288 }
289
290 pub fn deinit(_: *ModuleOnlyBackend) void {}
291
292 pub fn verify(_: *ModuleOnlyBackend, _: *ir.Operation) BackendError!void {}
293
294 pub fn lower(_: *ModuleOnlyBackend, module: *ir.Operation) BackendError!*ir.Operation {
295 return module;
296 }
297
298 pub fn emit(_: *ModuleOnlyBackend, _: *ir.Operation, _: EmitOptions, _: *std.Io.Writer) BackendError!void {}
299 };
300
301 test "module backend handle does not require debug services" {
302 var ctx = try ir.Context.init(std.testing.allocator, ir.Context.Limits.testing);
303 defer ctx.deinit(std.testing.allocator);
304 try @import("../dialects/root.zig").registerAllDialects(&ctx);
305
306 var handle = try initHandle(
307 ModuleOnlyBackend,
308 std.testing.allocator,
309 &ctx,
310 BackendTarget.external("test.module"),
311 "module-only",
312 .{ .artifact = .{ .source_text = true } },
313 );
314 defer handle.deinit();
315
316 try std.testing.expect(handle.debug == null);
317 try std.testing.expectError(BackendError.UnsupportedArchitecture, handle.disassemble(&.{}, undefined));
318 }
319
320 test "backend handle rejects unsupported claimed debug services" {
321 var ctx = try ir.Context.init(std.testing.allocator, ir.Context.Limits.testing);
322 defer ctx.deinit(std.testing.allocator);
323 try @import("../dialects/root.zig").registerAllDialects(&ctx);
324
325 try std.testing.expectError(
326 BackendError.UnsupportedArchitecture,
327 initHandle(
328 ModuleOnlyBackend,
329 std.testing.allocator,
330 &ctx,
331 BackendTarget.external("test.module"),
332 "module-only",
333 .{ .cpu = .{ .disassemble = true } },
334 ),
335 );
336 }