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 }