lib/choir/src/passes/plan.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const pass_mod = @import("pass/root.zig");
  3 const registry_mod = @import("pipeline.zig");
  4 
  5 pub const Target = union(enum) {
  6     any,
  7     op: []const u8,
  8 };
  9 
 10 pub const Invocation = struct {
 11     name: []const u8,
 12     options: []const registry_mod.PassOptionAssignment = &.{},
 13     text: ?[]const u8 = null,
 14 
 15     pub fn named(name: []const u8) Invocation {
 16         return .{ .name = name };
 17     }
 18 
 19     pub fn configured(
 20         name: []const u8,
 21         options: []const registry_mod.PassOptionAssignment,
 22     ) Invocation {
 23         return .{
 24             .name = name,
 25             .options = options,
 26         };
 27     }
 28 
 29     pub fn textual(
 30         name: []const u8,
 31         text: []const u8,
 32         options: []const registry_mod.PassOptionAssignment,
 33     ) Invocation {
 34         return .{
 35             .name = name,
 36             .options = options,
 37             .text = text,
 38         };
 39     }
 40 
 41     fn hasOptions(self: Invocation) bool {
 42         return self.text != null or self.options.len != 0;
 43     }
 44 };
 45 
 46 pub const Nested = struct {
 47     target: Target,
 48     steps: []const Step,
 49 };
 50 
 51 pub const Step = union(enum) {
 52     pass: Invocation,
 53     pipeline: Invocation,
 54     nested: Nested,
 55 
 56     pub fn namedPass(name: []const u8) Step {
 57         return .{ .pass = Invocation.named(name) };
 58     }
 59 
 60     pub fn configuredPass(
 61         name: []const u8,
 62         options: []const registry_mod.PassOptionAssignment,
 63     ) Step {
 64         return .{ .pass = Invocation.configured(name, options) };
 65     }
 66 
 67     pub fn namedPipeline(name: []const u8) Step {
 68         return .{ .pipeline = Invocation.named(name) };
 69     }
 70 
 71     pub fn configuredPipeline(
 72         name: []const u8,
 73         options: []const registry_mod.PassOptionAssignment,
 74     ) Step {
 75         return .{ .pipeline = Invocation.configured(name, options) };
 76     }
 77 
 78     pub fn textualPipeline(
 79         name: []const u8,
 80         text: []const u8,
 81         options: []const registry_mod.PassOptionAssignment,
 82     ) Step {
 83         return .{ .pipeline = Invocation.textual(name, text, options) };
 84     }
 85 
 86     pub fn nestedAny(steps: []const Step) Step {
 87         return .{
 88             .nested = .{
 89                 .target = .any,
 90                 .steps = steps,
 91             },
 92         };
 93     }
 94 
 95     pub fn nestedOp(name: []const u8, steps: []const Step) Step {
 96         return .{
 97             .nested = .{
 98                 .target = .{ .op = name },
 99                 .steps = steps,
100             },
101         };
102     }
103 };
104 
105 pub const Plan = struct {
106     steps: []const Step,
107 
108     pub fn addTo(
109         self: Plan,
110         registry: *const registry_mod.PassRegistry,
111         manager: *pass_mod.PassManager,
112     ) anyerror!void {
113         try self.addToOp(registry, &manager.root);
114     }
115 
116     pub fn addToOp(
117         self: Plan,
118         registry: *const registry_mod.PassRegistry,
119         manager: *pass_mod.OpPassManager,
120     ) anyerror!void {
121         var parsed = pass_mod.OpPassManager.initWithTarget(
122             manager.allocator,
123             manager.target_kind,
124             manager.target_op_name,
125         );
126         defer parsed.deinit();
127 
128         try applySteps(self.steps, registry, &parsed);
129         try appendParsedManager(manager, &parsed);
130     }
131 
132     pub fn write(self: Plan, writer: *std.Io.Writer) std.Io.Writer.Error!void {
133         try writeSteps(writer, self.steps);
134     }
135 
136     pub fn formatAlloc(self: Plan, allocator: std.mem.Allocator) ![]u8 {
137         var out = std.Io.Writer.Allocating.init(allocator);
138         defer out.deinit();
139         try self.write(&out.writer);
140         return try out.toOwnedSlice();
141     }
142 };
143 
144 fn applySteps(
145     steps: []const Step,
146     registry: *const registry_mod.PassRegistry,
147     manager: *pass_mod.OpPassManager,
148 ) anyerror!void {
149     for (steps) |step| {
150         switch (step) {
151             .pass => |invocation| try applyPass(invocation, registry, manager),
152             .pipeline => |invocation| try applyPipeline(invocation, registry, manager),
153             .nested => |nested| {
154                 const nested_manager = switch (nested.target) {
155                     .any => try manager.nestAny(),
156                     .op => |name| try manager.nest(name),
157                 };
158                 try applySteps(nested.steps, registry, nested_manager);
159             },
160         }
161     }
162 }
163 
164 fn applyPipeline(
165     invocation: Invocation,
166     registry: *const registry_mod.PassRegistry,
167     manager: *pass_mod.OpPassManager,
168 ) anyerror!void {
169     const registration = registry.lookupPipeline(invocation.name) orelse return error.UnknownPipeline;
170     if (!invocation.hasOptions()) {
171         try registration.addTo(manager);
172         return;
173     }
174 
175     try registration.addToWithOptions(
176         manager,
177         .{ .assignments = invocation.options },
178     );
179 }
180 
181 fn applyPass(
182     invocation: Invocation,
183     registry: *const registry_mod.PassRegistry,
184     manager: *pass_mod.OpPassManager,
185 ) anyerror!void {
186     const registration = registry.lookupPass(invocation.name) orelse return error.UnknownPass;
187     if (!invocation.hasOptions()) {
188         try registration.addTo(manager);
189         return;
190     }
191 
192     var owned_text: ?[]u8 = null;
193     defer if (owned_text) |text| manager.allocator.free(text);
194     const option_text = invocation.text orelse blk: {
195         owned_text = try formatOptionsAlloc(manager.allocator, invocation.options);
196         break :blk owned_text.?;
197     };
198 
199     try registration.addToWithOptions(
200         manager,
201         option_text,
202         .{ .assignments = invocation.options },
203     );
204 }
205 
206 fn appendParsedManager(
207     dst: *pass_mod.OpPassManager,
208     src: *pass_mod.OpPassManager,
209 ) std.mem.Allocator.Error!void {
210     try dst.pipeline.ensureUnusedCapacity(dst.allocator, src.pipeline.items.len);
211     try dst.nested_managers.ensureUnusedCapacity(dst.allocator, src.nested_managers.items.len);
212 
213     for (src.nested_managers.items) |nested| {
214         nested.parent = dst;
215     }
216     dst.pipeline.appendSliceAssumeCapacity(src.pipeline.items);
217     dst.nested_managers.appendSliceAssumeCapacity(src.nested_managers.items);
218 
219     src.pipeline.clearRetainingCapacity();
220     src.nested_managers.clearRetainingCapacity();
221 }
222 
223 fn writeSteps(writer: *std.Io.Writer, steps: []const Step) std.Io.Writer.Error!void {
224     for (steps, 0..) |step, index| {
225         if (index != 0) try writer.writeByte(',');
226         switch (step) {
227             .pass => |invocation| try writeInvocation(writer, invocation),
228             .pipeline => |invocation| try writeInvocation(writer, invocation),
229             .nested => |nested| {
230                 switch (nested.target) {
231                     .any => try writer.writeAll("any"),
232                     .op => |name| try writer.writeAll(name),
233                 }
234                 try writer.writeByte('(');
235                 try writeSteps(writer, nested.steps);
236                 try writer.writeByte(')');
237             },
238         }
239     }
240 }
241 
242 fn writeInvocation(writer: *std.Io.Writer, invocation: Invocation) std.Io.Writer.Error!void {
243     try writer.writeAll(invocation.name);
244     if (invocation.text) |text| {
245         try writer.writeByte('{');
246         try writer.writeAll(text);
247         try writer.writeByte('}');
248     } else if (invocation.options.len != 0) {
249         try writer.writeByte('{');
250         try writeOptions(writer, invocation.options);
251         try writer.writeByte('}');
252     }
253 }
254 
255 pub fn formatOptionsAlloc(
256     allocator: std.mem.Allocator,
257     options: []const registry_mod.PassOptionAssignment,
258 ) ![]u8 {
259     var out = std.Io.Writer.Allocating.init(allocator);
260     defer out.deinit();
261     try writeOptions(&out.writer, options);
262     return try out.toOwnedSlice();
263 }
264 
265 pub fn writeOptions(
266     writer: *std.Io.Writer,
267     options: []const registry_mod.PassOptionAssignment,
268 ) std.Io.Writer.Error!void {
269     for (options, 0..) |option, index| {
270         if (index != 0) try writer.writeByte(',');
271         try writer.writeAll(option.name);
272         if (option.value.len == 0) continue;
273         try writer.writeByte('=');
274         try writeOptionValue(writer, option.value);
275     }
276 }
277 
278 fn writeOptionValue(writer: *std.Io.Writer, value: []const u8) std.Io.Writer.Error!void {
279     if (!optionValueNeedsQuotes(value)) {
280         try writer.writeAll(value);
281         return;
282     }
283     try writer.writeByte('"');
284     try writer.writeAll(value);
285     try writer.writeByte('"');
286 }
287 
288 fn optionValueNeedsQuotes(value: []const u8) bool {
289     if (value.len == 0) return true;
290     for (value) |byte| {
291         if (std.ascii.isWhitespace(byte) or byte == ',' or byte == '}') return true;
292     }
293     return false;
294 }
295 
296 fn noopPass(_: *pass_mod.PassContext) pass_mod.PassResult {
297     return .success;
298 }
299 
300 const test_pass = pass_mod.Pass{
301     .name = "choir-plan-test-pass",
302     .description = "test pass for pass plans",
303     .run_fn = noopPass,
304 };
305 
306 const test_other_pass = pass_mod.Pass{
307     .name = "choir-plan-test-other-pass",
308     .description = "second test pass for pass plans",
309     .run_fn = noopPass,
310 };
311 
312 const test_option_pass = pass_mod.Pass{
313     .name = "choir-plan-test-option-pass",
314     .description = "test pass for pass plan options",
315     .run_fn = noopPass,
316 };
317 
318 const test_option_specs = [_]registry_mod.PassOptionSpec{
319     .{
320         .name = "limit",
321         .description = "test limit",
322         .kind = .unsigned,
323         .default_value = "0",
324     },
325     .{
326         .name = "enabled",
327         .description = "test toggle",
328         .kind = .boolean,
329         .default_value = "false",
330     },
331 };
332 
333 fn buildTestOptionPass(_: std.mem.Allocator, options: registry_mod.PassOptionSet) anyerror!pass_mod.Pass {
334     _ = try options.unsignedValue(usize, "limit", 0);
335     _ = try options.boolValue("enabled", false);
336     return test_option_pass;
337 }
338 
339 fn buildTestPipeline(manager: *pass_mod.OpPassManager) anyerror!void {
340     try manager.addPass(test_pass);
341 }
342 
343 const test_pass_registration = registry_mod.PassRegistration{
344     .name = test_pass.name,
345     .description = test_pass.description,
346     .pass = test_pass,
347 };
348 
349 const test_other_pass_registration = registry_mod.PassRegistration{
350     .name = test_other_pass.name,
351     .description = test_other_pass.description,
352     .pass = test_other_pass,
353 };
354 
355 const test_option_pass_registration = registry_mod.PassRegistration{
356     .name = test_option_pass.name,
357     .description = test_option_pass.description,
358     .pass = test_option_pass,
359     .options = &test_option_specs,
360     .build_with_options = buildTestOptionPass,
361 };
362 
363 const test_pipeline = registry_mod.PipelineRegistration{
364     .name = "choir-plan-test-pipeline",
365     .description = "test pipeline for pass plans",
366     .build = buildTestPipeline,
367 };
368 
369 fn buildTestPipelineWithOptions(manager: *pass_mod.OpPassManager, options: registry_mod.PassOptionSet) anyerror!void {
370     const text = try formatOptionsAlloc(manager.allocator, options.assignments);
371     defer manager.allocator.free(text);
372     try test_option_pass_registration.addToWithOptions(manager, text, options);
373 }
374 
375 const test_option_pipeline = registry_mod.PipelineRegistration{
376     .name = "choir-plan-test-option-pipeline",
377     .description = "test pipeline with options",
378     .build = buildTestPipeline,
379     .options = &test_option_specs,
380     .build_with_options = buildTestPipelineWithOptions,
381 };
382 
383 fn buildTestRegistry(allocator: std.mem.Allocator) !registry_mod.PassRegistry {
384     var registry = registry_mod.PassRegistry.init(allocator);
385     errdefer registry.deinit();
386     try registry.registerPass(test_pass_registration);
387     try registry.registerPass(test_other_pass_registration);
388     try registry.registerPass(test_option_pass_registration);
389     try registry.registerPipeline(test_pipeline);
390     try registry.registerPipeline(test_option_pipeline);
391     return registry;
392 }
393 
394 test "pass plan materializes named passes and pipelines" {
395     var registry = try buildTestRegistry(std.testing.allocator);
396     defer registry.deinit();
397 
398     const steps = [_]Step{
399         Step.namedPass(test_pass.name),
400         Step.namedPipeline(test_pipeline.name),
401     };
402     const pass_plan = Plan{ .steps = &steps };
403 
404     const plan_text = try pass_plan.formatAlloc(std.testing.allocator);
405     defer std.testing.allocator.free(plan_text);
406     try std.testing.expectEqualStrings("choir-plan-test-pass,choir-plan-test-pipeline", plan_text);
407 
408     var manager = pass_mod.PassManager.init(std.testing.allocator);
409     defer manager.deinit();
410     try pass_plan.addTo(&registry, &manager);
411 
412     try std.testing.expectEqual(@as(usize, 2), manager.root.pipeline.items.len);
413     switch (manager.root.pipeline.items[0]) {
414         .pass => |pass| try std.testing.expectEqualStrings(test_pass.name, pass.name),
415         .nested => return error.TestExpectedPass,
416     }
417     switch (manager.root.pipeline.items[1]) {
418         .pass => |pass| try std.testing.expectEqualStrings(test_pass.name, pass.name),
419         .nested => return error.TestExpectedPass,
420     }
421 }
422 
423 test "pass plan materializes configured pipelines" {
424     var registry = try buildTestRegistry(std.testing.allocator);
425     defer registry.deinit();
426 
427     const options = [_]registry_mod.PassOptionAssignment{
428         .{ .name = "limit", .value = "9" },
429     };
430     const steps = [_]Step{
431         Step.configuredPipeline(test_option_pipeline.name, &options),
432     };
433     const pass_plan = Plan{ .steps = &steps };
434 
435     const plan_text = try pass_plan.formatAlloc(std.testing.allocator);
436     defer std.testing.allocator.free(plan_text);
437     try std.testing.expectEqualStrings("choir-plan-test-option-pipeline{limit=9}", plan_text);
438 
439     var manager = pass_mod.PassManager.init(std.testing.allocator);
440     defer manager.deinit();
441     try pass_plan.addTo(&registry, &manager);
442 
443     try std.testing.expectEqual(@as(usize, 1), manager.root.pipeline.items.len);
444     switch (manager.root.pipeline.items[0]) {
445         .pass => |pass| {
446             try std.testing.expectEqualStrings(test_option_pass.name, pass.name);
447             try std.testing.expectEqualStrings("limit=9", pass.textual_options.?);
448         },
449         .nested => return error.TestExpectedPass,
450     }
451 }
452 
453 test "pass plan formats bare boolean option flags" {
454     var registry = try buildTestRegistry(std.testing.allocator);
455     defer registry.deinit();
456 
457     const options = [_]registry_mod.PassOptionAssignment{
458         .{ .name = "enabled", .value = "" },
459         .{ .name = "limit", .value = "7" },
460     };
461     const steps = [_]Step{
462         Step.configuredPass(test_option_pass.name, &options),
463     };
464     const pass_plan = Plan{ .steps = &steps };
465 
466     const plan_text = try pass_plan.formatAlloc(std.testing.allocator);
467     defer std.testing.allocator.free(plan_text);
468     try std.testing.expectEqualStrings("choir-plan-test-option-pass{enabled,limit=7}", plan_text);
469 
470     var manager = pass_mod.PassManager.init(std.testing.allocator);
471     defer manager.deinit();
472     try pass_plan.addTo(&registry, &manager);
473 
474     switch (manager.root.pipeline.items[0]) {
475         .pass => |pass| try std.testing.expectEqualStrings("enabled,limit=7", pass.textual_options.?),
476         .nested => return error.TestExpectedPass,
477     }
478 }
479 
480 test "pass plan materializes nested configured passes" {
481     var registry = try buildTestRegistry(std.testing.allocator);
482     defer registry.deinit();
483 
484     const options = [_]registry_mod.PassOptionAssignment{
485         .{ .name = "limit", .value = "7" },
486     };
487     const nested_steps = [_]Step{
488         Step.configuredPass(test_option_pass.name, &options),
489     };
490     const steps = [_]Step{
491         Step.nestedOp("test.op", &nested_steps),
492         Step.nestedAny(&.{Step.namedPass(test_other_pass.name)}),
493     };
494     const pass_plan = Plan{ .steps = &steps };
495 
496     const plan_text = try pass_plan.formatAlloc(std.testing.allocator);
497     defer std.testing.allocator.free(plan_text);
498     try std.testing.expectEqualStrings("test.op(choir-plan-test-option-pass{limit=7}),any(choir-plan-test-other-pass)", plan_text);
499 
500     var manager = pass_mod.PassManager.init(std.testing.allocator);
501     defer manager.deinit();
502     try pass_plan.addTo(&registry, &manager);
503 
504     try std.testing.expectEqual(@as(usize, 2), manager.root.pipeline.items.len);
505     switch (manager.root.pipeline.items[0]) {
506         .nested => |nested| {
507             try std.testing.expectEqual(pass_mod.OpPassManagerTargetKind.op, nested.target_kind);
508             try std.testing.expectEqualStrings("test.op", nested.target_op_name.?);
509             switch (nested.pipeline.items[0]) {
510                 .pass => |pass| {
511                     try std.testing.expectEqualStrings(test_option_pass.name, pass.name);
512                     try std.testing.expectEqualStrings("limit=7", pass.textual_options.?);
513                 },
514                 .nested => return error.TestExpectedPass,
515             }
516         },
517         .pass => return error.TestExpectedNestedPassManager,
518     }
519     switch (manager.root.pipeline.items[1]) {
520         .nested => |nested| {
521             try std.testing.expectEqual(pass_mod.OpPassManagerTargetKind.any, nested.target_kind);
522             try std.testing.expectEqual(@as(?[]const u8, null), nested.target_op_name);
523         },
524         .pass => return error.TestExpectedNestedPassManager,
525     }
526 }
527 
528 test "pass plan errors do not mutate destination manager" {
529     var registry = try buildTestRegistry(std.testing.allocator);
530     defer registry.deinit();
531 
532     const existing_steps = [_]Step{Step.namedPass(test_pass.name)};
533     const existing_plan = Plan{ .steps = &existing_steps };
534     var manager = pass_mod.PassManager.init(std.testing.allocator);
535     defer manager.deinit();
536     try existing_plan.addTo(&registry, &manager);
537 
538     const bad_steps = [_]Step{
539         Step.namedPass(test_other_pass.name),
540         Step.namedPass("missing-pass"),
541     };
542     const bad_plan = Plan{ .steps = &bad_steps };
543     try std.testing.expectError(error.UnknownPass, bad_plan.addTo(&registry, &manager));
544 
545     try std.testing.expectEqual(@as(usize, 1), manager.root.pipeline.items.len);
546     switch (manager.root.pipeline.items[0]) {
547         .pass => |pass| try std.testing.expectEqualStrings(test_pass.name, pass.name),
548         .nested => return error.TestExpectedPass,
549     }
550 }