lib/choir/src/backends/translation.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const ir = @import("../core/root.zig");
  3 const artifact_mod = @import("root.zig").artifact;
  4 
  5 const Allocator = std.mem.Allocator;
  6 
  7 pub const Error = Allocator.Error || error{
  8     DuplicateTranslation,
  9     InvalidTranslation,
 10     UnknownTranslation,
 11 };
 12 
 13 pub const Request = struct {
 14     module: *ir.Operation,
 15     entry: ?[]const u8 = null,
 16 };
 17 
 18 pub const EmitBytesFn = *const fn (Allocator, Request) anyerror![]u8;
 19 
 20 pub const Descriptor = struct {
 21     name: []const u8,
 22     target_dialect_name: []const u8,
 23     format_name: []const u8,
 24     artifact_kind: artifact_mod.ArtifactKind = .unknown,
 25     buffer_format: artifact_mod.BufferFormat = .vendor,
 26 };
 27 
 28 pub const Registration = struct {
 29     name: []const u8,
 30     target_dialect_name: []const u8,
 31     format_name: []const u8,
 32     artifact_kind: artifact_mod.ArtifactKind = .unknown,
 33     buffer_format: artifact_mod.BufferFormat = .vendor,
 34     emit_bytes: EmitBytesFn,
 35 };
 36 
 37 pub fn registration(comptime Impl: type, descriptor: Descriptor) Registration {
 38     comptime {
 39         if (!@hasDecl(Impl, "emitBytes")) @compileError("backend translation missing emitBytes");
 40         const emit_bytes: EmitBytesFn = Impl.emitBytes;
 41         _ = emit_bytes;
 42     }
 43 
 44     return .{
 45         .name = descriptor.name,
 46         .target_dialect_name = descriptor.target_dialect_name,
 47         .format_name = descriptor.format_name,
 48         .artifact_kind = descriptor.artifact_kind,
 49         .buffer_format = descriptor.buffer_format,
 50         .emit_bytes = Impl.emitBytes,
 51     };
 52 }
 53 
 54 pub const Registry = struct {
 55     allocator: Allocator,
 56     registrations: std.ArrayListUnmanaged(Registration) = .empty,
 57 
 58     pub fn init(allocator: Allocator) Registry {
 59         return .{ .allocator = allocator };
 60     }
 61 
 62     pub fn deinit(self: *Registry) void {
 63         self.registrations.deinit(self.allocator);
 64         self.* = undefined;
 65     }
 66 
 67     pub fn register(self: *Registry, item: Registration) Error!void {
 68         try validate(item);
 69         if (self.lookup(item.name) != null) return error.DuplicateTranslation;
 70         if (self.lookupTargetFormat(item.target_dialect_name, item.format_name) != null) return error.DuplicateTranslation;
 71         try self.registrations.ensureTotalCapacity(self.allocator, self.registrations.items.len + 1);
 72         self.registrations.appendAssumeCapacity(item);
 73     }
 74 
 75     pub fn registerAll(self: *Registry, items: []const Registration) Error!void {
 76         for (items) |item| {
 77             try self.register(item);
 78         }
 79     }
 80 
 81     pub fn lookup(self: *const Registry, name: []const u8) ?Registration {
 82         for (self.registrations.items) |item| {
 83             if (std.mem.eql(u8, item.name, name)) return item;
 84         }
 85         return null;
 86     }
 87 
 88     pub fn lookupTargetFormat(self: *const Registry, target_dialect_name: []const u8, format_name: []const u8) ?Registration {
 89         for (self.registrations.items) |item| {
 90             if (std.mem.eql(u8, item.target_dialect_name, target_dialect_name) and
 91                 std.mem.eql(u8, item.format_name, format_name))
 92             {
 93                 return item;
 94             }
 95         }
 96         return null;
 97     }
 98 
 99     pub fn emitBytes(
100         self: *const Registry,
101         result_allocator: Allocator,
102         target_dialect_name: []const u8,
103         format_name: []const u8,
104         request: Request,
105     ) anyerror![]u8 {
106         const item = self.lookupTargetFormat(target_dialect_name, format_name) orelse return error.UnknownTranslation;
107         return item.emit_bytes(result_allocator, request);
108     }
109 
110     pub fn emitArtifact(
111         self: *const Registry,
112         result_allocator: Allocator,
113         target_dialect_name: []const u8,
114         format_name: []const u8,
115         request: Request,
116     ) anyerror!artifact_mod.Artifact {
117         const item = self.lookupTargetFormat(target_dialect_name, format_name) orelse return error.UnknownTranslation;
118         const bytes = try item.emit_bytes(result_allocator, request);
119         defer result_allocator.free(bytes);
120 
121         var artifact = try artifact_mod.Artifact.init(result_allocator, .{
122             .kind = item.artifact_kind,
123             .producer = item.name,
124         });
125         errdefer artifact.deinit();
126 
127         try artifact.payload.addBuffer(.{
128             .name = item.format_name,
129             .format = item.buffer_format,
130             .bytes = bytes,
131         });
132         return artifact;
133     }
134 };
135 
136 fn validate(item: Registration) Error!void {
137     if (item.name.len == 0) return error.InvalidTranslation;
138     if (item.target_dialect_name.len == 0) return error.InvalidTranslation;
139     if (item.format_name.len == 0) return error.InvalidTranslation;
140 }
141 
142 const FakeTranslation = struct {
143     pub fn emitBytes(result_allocator: Allocator, request: Request) anyerror![]u8 {
144         _ = request;
145         return result_allocator.dupe(u8, "fake-bytes");
146     }
147 };
148 
149 const fake_registration = registration(FakeTranslation, .{
150     .name = "fake.binary",
151     .target_dialect_name = "fake",
152     .format_name = "fake-binary",
153 });
154 
155 test "backend translation registry dispatches by target dialect and format" {
156     var registry = Registry.init(std.testing.allocator);
157     defer registry.deinit();
158 
159     try registry.register(fake_registration);
160     try std.testing.expect(registry.lookup("fake.binary") != null);
161     try std.testing.expect(registry.lookupTargetFormat("fake", "fake-binary") != null);
162     try std.testing.expect(registry.lookupTargetFormat("fake", "missing") == null);
163 
164     var ctx = try ir.Context.init(std.testing.allocator, ir.Context.Limits.testing);
165     defer ctx.deinit(std.testing.allocator);
166     try ctx.allowUnregistered();
167 
168     const op = try ctx.createOperation(ir.Operation.State.init("fake.op", .unknown));
169 
170     const bytes = try registry.emitBytes(std.testing.allocator, "fake", "fake-binary", .{ .module = op });
171     defer std.testing.allocator.free(bytes);
172     try std.testing.expectEqualStrings("fake-bytes", bytes);
173 }
174 
175 test "backend translation registry rejects duplicate registrations" {
176     var registry = Registry.init(std.testing.allocator);
177     defer registry.deinit();
178 
179     try registry.register(fake_registration);
180     try std.testing.expectError(error.DuplicateTranslation, registry.register(fake_registration));
181 
182     const duplicate_target = registration(FakeTranslation, .{
183         .name = "fake.second",
184         .target_dialect_name = "fake",
185         .format_name = "fake-binary",
186     });
187     try std.testing.expectError(error.DuplicateTranslation, registry.register(duplicate_target));
188 }
189 
190 test "backend translation registry emits artifacts" {
191     var registry = Registry.init(std.testing.allocator);
192     defer registry.deinit();
193 
194     const artifact_registration = registration(FakeTranslation, .{
195         .name = "fake.artifact",
196         .target_dialect_name = "fake",
197         .format_name = "fake-object",
198         .artifact_kind = .object_file,
199         .buffer_format = .object_file,
200     });
201     try registry.register(artifact_registration);
202 
203     var ctx = try ir.Context.init(std.testing.allocator, ir.Context.Limits.testing);
204     defer ctx.deinit(std.testing.allocator);
205     try ctx.allowUnregistered();
206 
207     const op = try ctx.createOperation(ir.Operation.State.init("fake.op", .unknown));
208 
209     var artifact = try registry.emitArtifact(std.testing.allocator, "fake", "fake-object", .{ .module = op });
210     defer artifact.deinit();
211 
212     try std.testing.expectEqual(artifact_mod.ArtifactKind.object_file, artifact.metadata.kind);
213     try std.testing.expectEqual(@as(usize, 1), artifact.payload.buffers.items.len);
214     try std.testing.expectEqualStrings("fake-object", artifact.payload.buffers.items[0].name);
215     try std.testing.expectEqualStrings("fake-bytes", artifact.payload.buffers.items[0].bytes);
216 }
217 
218 test "backend translation registry reports unknown targets" {
219     var registry = Registry.init(std.testing.allocator);
220     defer registry.deinit();
221 
222     var ctx = try ir.Context.init(std.testing.allocator, ir.Context.Limits.testing);
223     defer ctx.deinit(std.testing.allocator);
224     try ctx.allowUnregistered();
225 
226     const op = try ctx.createOperation(ir.Operation.State.init("fake.op", .unknown));
227     try std.testing.expectError(
228         error.UnknownTranslation,
229         registry.emitBytes(std.testing.allocator, "missing", "fake-binary", .{ .module = op }),
230     );
231 }