lib/choir/src/passes/pipeline.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const options_mod = @import("options.zig");
  3 const pass_mod = @import("pass/root.zig");
  4 
  5 pub const PipelineError = error{
  6     DuplicatePipeline,
  7     UnknownPipeline,
  8 };
  9 
 10 pub const RegistryError = PipelineError || error{
 11     DuplicatePass,
 12     UnknownPass,
 13 };
 14 
 15 pub const PassOptionSpec = options_mod.PassOptionSpec;
 16 pub const PassOptionChoice = options_mod.PassOptionChoice;
 17 pub const PassOptionAssignment = options_mod.PassOptionAssignment;
 18 pub const PassOptionSet = options_mod.PassOptionSet;
 19 pub const PassOptionsBuilderFn = *const fn (std.mem.Allocator, PassOptionSet) anyerror!pass_mod.Pass;
 20 pub const PipelineOptionsBuilderFn = *const fn (*pass_mod.OpPassManager, PassOptionSet) anyerror!void;
 21 
 22 pub const PassRegistration = struct {
 23     name: []const u8,
 24     description: []const u8,
 25     pass: pass_mod.Pass,
 26     target_op_name: ?[]const u8 = null,
 27     options: []const PassOptionSpec = &.{},
 28     build_with_options: ?PassOptionsBuilderFn = null,
 29 
 30     pub fn addTo(self: PassRegistration, manager: *pass_mod.OpPassManager) anyerror!void {
 31         if (self.target_op_name) |target| {
 32             const nested = try manager.nest(target);
 33             try nested.addPass(self.pass);
 34         } else {
 35             try manager.addPass(self.pass);
 36         }
 37     }
 38 
 39     pub fn addToWithOptions(
 40         self: PassRegistration,
 41         manager: *pass_mod.OpPassManager,
 42         option_text: []const u8,
 43         option_set: PassOptionSet,
 44     ) anyerror!void {
 45         const build = self.build_with_options orelse return error.PassOptionsNotSupported;
 46         try options_mod.validatePassOptions(self.options, option_set);
 47         const target_manager = if (self.target_op_name) |target|
 48             try manager.nest(target)
 49         else
 50             manager;
 51         var pass = try build(manager.allocator, option_set);
 52         errdefer pass.deinit(manager.allocator);
 53         if (option_text.len != 0) {
 54             pass.textual_options = try manager.allocator.dupe(u8, option_text);
 55             pass.owns_textual_options = true;
 56         } else {
 57             pass.textual_options = "";
 58         }
 59         try target_manager.addPass(pass);
 60     }
 61 };
 62 
 63 pub const PipelineBuilderFn = *const fn (*pass_mod.OpPassManager) anyerror!void;
 64 
 65 pub const PipelineRegistration = struct {
 66     name: []const u8,
 67     description: []const u8,
 68     build: PipelineBuilderFn,
 69     options: []const PassOptionSpec = &.{},
 70     build_with_options: ?PipelineOptionsBuilderFn = null,
 71 
 72     pub fn addTo(self: PipelineRegistration, manager: *pass_mod.OpPassManager) anyerror!void {
 73         try self.build(manager);
 74     }
 75 
 76     pub fn addToWithOptions(
 77         self: PipelineRegistration,
 78         manager: *pass_mod.OpPassManager,
 79         option_set: PassOptionSet,
 80     ) anyerror!void {
 81         const build = self.build_with_options orelse return error.PassOptionsNotSupported;
 82         try options_mod.validatePassOptions(self.options, option_set);
 83         try build(manager, option_set);
 84     }
 85 };
 86 
 87 pub const PassRegistry = struct {
 88     allocator: std.mem.Allocator,
 89     passes: std.ArrayListUnmanaged(PassRegistration) = .empty,
 90     pipelines: std.ArrayListUnmanaged(PipelineRegistration) = .empty,
 91 
 92     pub fn init(allocator: std.mem.Allocator) PassRegistry {
 93         return .{ .allocator = allocator };
 94     }
 95 
 96     pub fn deinit(self: *PassRegistry) void {
 97         self.passes.deinit(self.allocator);
 98         self.pipelines.deinit(self.allocator);
 99     }
100 
101     pub fn registerPass(
102         self: *PassRegistry,
103         registration: PassRegistration,
104     ) (std.mem.Allocator.Error || RegistryError)!void {
105         if (self.lookupPass(registration.name) != null) return error.DuplicatePass;
106         if (self.lookupPipeline(registration.name) != null) return error.DuplicatePass;
107         try self.passes.append(self.allocator, registration);
108     }
109 
110     pub fn registerPipeline(
111         self: *PassRegistry,
112         registration: PipelineRegistration,
113     ) (std.mem.Allocator.Error || RegistryError)!void {
114         if (self.lookupPipeline(registration.name) != null) return error.DuplicatePipeline;
115         if (self.lookupPass(registration.name) != null) return error.DuplicatePipeline;
116         try self.pipelines.append(self.allocator, registration);
117     }
118 
119     pub fn entryCount(self: *const PassRegistry) usize {
120         return self.passes.items.len + self.pipelines.items.len;
121     }
122 
123     pub fn lookupPass(self: *const PassRegistry, name: []const u8) ?PassRegistration {
124         for (self.passes.items) |registration| {
125             if (std.mem.eql(u8, registration.name, name)) return registration;
126         }
127         return null;
128     }
129 
130     pub fn lookupPipeline(self: *const PassRegistry, name: []const u8) ?PipelineRegistration {
131         for (self.pipelines.items) |registration| {
132             if (std.mem.eql(u8, registration.name, name)) return registration;
133         }
134         return null;
135     }
136 
137     pub fn addPassTo(
138         self: *const PassRegistry,
139         name: []const u8,
140         manager: *pass_mod.PassManager,
141     ) anyerror!void {
142         const registration = self.lookupPass(name) orelse return error.UnknownPass;
143         try registration.addTo(&manager.root);
144     }
145 
146     pub fn addPipelineTo(
147         self: *const PassRegistry,
148         name: []const u8,
149         manager: *pass_mod.PassManager,
150     ) anyerror!void {
151         const registration = self.lookupPipeline(name) orelse return error.UnknownPipeline;
152         try registration.addTo(&manager.root);
153     }
154 
155     pub fn writeHelp(self: *const PassRegistry, writer: *std.Io.Writer) std.Io.Writer.Error!void {
156         try writer.writeAll("passes:\n");
157         if (self.passes.items.len == 0) {
158             try writer.writeAll("  (none)\n");
159         } else {
160             for (self.passes.items) |registration| {
161                 try writer.print("  {s} - {s}\n", .{ registration.name, registration.description });
162                 for (registration.options) |option| {
163                     try writer.print("    {s} - {s}\n", .{ option.name, option.description });
164                 }
165             }
166         }
167 
168         try writer.writeAll("pipelines:\n");
169         if (self.pipelines.items.len == 0) {
170             try writer.writeAll("  (none)\n");
171         } else {
172             for (self.pipelines.items) |registration| {
173                 try writer.print("  {s} - {s}\n", .{ registration.name, registration.description });
174                 for (registration.options) |option| {
175                     try writer.print("    {s} - {s}\n", .{ option.name, option.description });
176                 }
177             }
178         }
179     }
180 
181     pub fn formatHelpAlloc(self: *const PassRegistry, allocator: std.mem.Allocator) ![]u8 {
182         var out = std.Io.Writer.Allocating.init(allocator);
183         defer out.deinit();
184         try self.writeHelp(&out.writer);
185         return try out.toOwnedSlice();
186     }
187 };
188 
189 pub const PipelineRegistry = struct {
190     allocator: std.mem.Allocator,
191     entries: std.ArrayListUnmanaged(PipelineRegistration) = .empty,
192 
193     pub fn init(allocator: std.mem.Allocator) PipelineRegistry {
194         return .{ .allocator = allocator };
195     }
196 
197     pub fn deinit(self: *PipelineRegistry) void {
198         self.entries.deinit(self.allocator);
199     }
200 
201     pub fn registerPipeline(
202         self: *PipelineRegistry,
203         registration: PipelineRegistration,
204     ) (std.mem.Allocator.Error || PipelineError)!void {
205         if (self.lookup(registration.name) != null) return error.DuplicatePipeline;
206         try self.entries.append(self.allocator, registration);
207     }
208 
209     pub fn lookup(self: *const PipelineRegistry, name: []const u8) ?PipelineRegistration {
210         for (self.entries.items) |registration| {
211             if (std.mem.eql(u8, registration.name, name)) return registration;
212         }
213         return null;
214     }
215 
216     pub fn addPipelineTo(
217         self: *const PipelineRegistry,
218         name: []const u8,
219         manager: *pass_mod.PassManager,
220     ) anyerror!void {
221         const registration = self.lookup(name) orelse return error.UnknownPipeline;
222         try registration.addTo(&manager.root);
223     }
224 
225     pub fn addPipelineToWithOptions(
226         self: *const PipelineRegistry,
227         name: []const u8,
228         manager: *pass_mod.PassManager,
229         option_set: PassOptionSet,
230     ) anyerror!void {
231         const registration = self.lookup(name) orelse return error.UnknownPipeline;
232         try registration.addToWithOptions(&manager.root, option_set);
233     }
234 };
235 
236 fn noopPass(_: *pass_mod.PassContext) pass_mod.PassResult {
237     return .success;
238 }
239 
240 const test_pass = pass_mod.Pass{
241     .name = "choir-pipeline-test-pass",
242     .description = "test pass for pipeline registry",
243     .run_fn = noopPass,
244 };
245 
246 const test_pass_registration = PassRegistration{
247     .name = "choir-test-pass",
248     .description = "test pass registration",
249     .pass = test_pass,
250 };
251 
252 const test_nested_pass_registration = PassRegistration{
253     .name = "choir-test-nested-pass",
254     .description = "test nested pass registration",
255     .pass = test_pass,
256     .target_op_name = "test.nested",
257 };
258 
259 fn buildTestPipeline(manager: *pass_mod.OpPassManager) anyerror!void {
260     try manager.addPass(test_pass);
261 }
262 
263 const test_pipeline = PipelineRegistration{
264     .name = "choir-test-pipeline",
265     .description = "test pipeline registration",
266     .build = buildTestPipeline,
267 };
268 
269 const test_pipeline_option_specs = [_]PassOptionSpec{
270     .{
271         .name = "limit",
272         .description = "pipeline limit",
273         .kind = .unsigned,
274     },
275 };
276 
277 fn buildTestPipelineWithOptions(manager: *pass_mod.OpPassManager, options: PassOptionSet) anyerror!void {
278     _ = try options.unsignedValue(usize, "limit", 0);
279     try manager.addPass(test_pass);
280     try manager.addPass(test_pass);
281 }
282 
283 const test_pipeline_with_options = PipelineRegistration{
284     .name = "choir-test-pipeline-options",
285     .description = "test pipeline registration with options",
286     .build = buildTestPipeline,
287     .options = &test_pipeline_option_specs,
288     .build_with_options = buildTestPipelineWithOptions,
289 };
290 
291 test "pipeline registry registers and applies named pipelines" {
292     var registry = PipelineRegistry.init(std.testing.allocator);
293     defer registry.deinit();
294 
295     try registry.registerPipeline(test_pipeline);
296     try std.testing.expect(registry.lookup(test_pipeline.name) != null);
297     try std.testing.expectError(error.DuplicatePipeline, registry.registerPipeline(test_pipeline));
298 
299     var manager = pass_mod.PassManager.init(std.testing.allocator);
300     defer manager.deinit();
301     try registry.addPipelineTo(test_pipeline.name, &manager);
302     try std.testing.expectEqual(@as(usize, 1), manager.root.pipeline.items.len);
303     try std.testing.expectError(error.UnknownPipeline, registry.addPipelineTo("missing", &manager));
304 }
305 
306 test "pass registry registers selected passes and pipelines by name" {
307     var registry = PassRegistry.init(std.testing.allocator);
308     defer registry.deinit();
309 
310     try registry.registerPass(test_pass_registration);
311     try registry.registerPipeline(test_pipeline);
312     try std.testing.expect(registry.lookupPass(test_pass_registration.name) != null);
313     try std.testing.expect(registry.lookupPipeline(test_pipeline.name) != null);
314     try std.testing.expectError(error.DuplicatePass, registry.registerPass(test_pass_registration));
315     try std.testing.expectError(error.DuplicatePipeline, registry.registerPipeline(test_pipeline));
316 
317     var manager = pass_mod.PassManager.init(std.testing.allocator);
318     defer manager.deinit();
319     try registry.addPassTo(test_pass_registration.name, &manager);
320     try registry.addPipelineTo(test_pipeline.name, &manager);
321     try std.testing.expectEqual(@as(usize, 2), manager.root.pipeline.items.len);
322     try std.testing.expectError(error.UnknownPass, registry.addPassTo("missing", &manager));
323     try std.testing.expectError(error.UnknownPipeline, registry.addPipelineTo("missing", &manager));
324 }
325 
326 test "pass registry keeps pass and pipeline names in one textual namespace" {
327     var registry = PassRegistry.init(std.testing.allocator);
328     defer registry.deinit();
329 
330     try registry.registerPass(test_pass_registration);
331     try std.testing.expectError(
332         error.DuplicatePipeline,
333         registry.registerPipeline(.{
334             .name = test_pass_registration.name,
335             .description = "pipeline colliding with a pass name",
336             .build = buildTestPipeline,
337         }),
338     );
339 
340     var other = PassRegistry.init(std.testing.allocator);
341     defer other.deinit();
342 
343     try other.registerPipeline(test_pipeline);
344     try std.testing.expectError(
345         error.DuplicatePass,
346         other.registerPass(.{
347             .name = test_pipeline.name,
348             .description = "pass colliding with a pipeline name",
349             .pass = test_pass,
350         }),
351     );
352 }
353 
354 test "pass registry writes inspectable pass and pipeline help" {
355     var registry = PassRegistry.init(std.testing.allocator);
356     defer registry.deinit();
357 
358     try registry.registerPass(test_pass_registration);
359     try registry.registerPipeline(test_pipeline);
360     try registry.registerPipeline(test_pipeline_with_options);
361     try std.testing.expectEqual(@as(usize, 3), registry.entryCount());
362 
363     const text = try registry.formatHelpAlloc(std.testing.allocator);
364     defer std.testing.allocator.free(text);
365 
366     try std.testing.expect(std.mem.indexOf(u8, text, "passes:\n") != null);
367     try std.testing.expect(std.mem.indexOf(u8, text, test_pass_registration.name) != null);
368     try std.testing.expect(std.mem.indexOf(u8, text, test_pass_registration.description) != null);
369     try std.testing.expect(std.mem.indexOf(u8, text, "pipelines:\n") != null);
370     try std.testing.expect(std.mem.indexOf(u8, text, test_pipeline.name) != null);
371     try std.testing.expect(std.mem.indexOf(u8, text, test_pipeline.description) != null);
372     try std.testing.expect(std.mem.indexOf(u8, text, test_pipeline_option_specs[0].name) != null);
373 }
374 
375 test "pass registry preserves target operation nesting" {
376     var registry = PassRegistry.init(std.testing.allocator);
377     defer registry.deinit();
378 
379     try registry.registerPass(test_nested_pass_registration);
380 
381     var manager = pass_mod.PassManager.init(std.testing.allocator);
382     defer manager.deinit();
383     try registry.addPassTo(test_nested_pass_registration.name, &manager);
384     try std.testing.expectEqual(@as(usize, 1), manager.root.pipeline.items.len);
385 
386     switch (manager.root.pipeline.items[0]) {
387         .nested => |nested| {
388             try std.testing.expectEqualStrings("test.nested", nested.target_op_name.?);
389             try std.testing.expectEqual(@as(usize, 1), nested.pipeline.items.len);
390         },
391         .pass => return error.TestExpectedNestedPassManager,
392     }
393 }
394 
395 test "pipeline registry validates and applies pipeline options" {
396     var registry = PassRegistry.init(std.testing.allocator);
397     defer registry.deinit();
398 
399     try registry.registerPipeline(test_pipeline);
400     try registry.registerPipeline(test_pipeline_with_options);
401 
402     var manager = pass_mod.PassManager.init(std.testing.allocator);
403     defer manager.deinit();
404 
405     const assignments = [_]PassOptionAssignment{
406         .{ .name = "limit", .value = "2" },
407     };
408     try registry.lookupPipeline(test_pipeline_with_options.name).?.addToWithOptions(
409         &manager.root,
410         .{ .assignments = &assignments },
411     );
412     try std.testing.expectEqual(@as(usize, 2), manager.root.pipeline.items.len);
413 
414     try std.testing.expectError(
415         error.UnknownPassOption,
416         registry.lookupPipeline(test_pipeline_with_options.name).?.addToWithOptions(
417             &manager.root,
418             .{ .assignments = &.{.{ .name = "missing", .value = "1" }} },
419         ),
420     );
421     try std.testing.expectError(
422         error.PassOptionsNotSupported,
423         registry.lookupPipeline(test_pipeline.name).?.addToWithOptions(
424             &manager.root,
425             .{ .assignments = &assignments },
426         ),
427     );
428 }