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 }