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(®istry, &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(®istry, &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(®istry, &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(®istry, &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(®istry, &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(®istry, &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 }