lib/choir/src/passes/textual.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 TextualPipelineError = error{
6 EmptyElement,
7 ExpectedComma,
8 ExpectedPassOptionName,
9 ExpectedPassOptionValue,
10 MissingCloseParen,
11 MissingCloseBrace,
12 PassOptionsNotSupported,
13 UnknownPassOrPipeline,
14 UnexpectedCloseParen,
15 };
16
17 pub fn parsePassPipeline(
18 registry: *const registry_mod.PassRegistry,
19 text: []const u8,
20 manager: *pass_mod.PassManager,
21 ) anyerror!void {
22 try parseOpPassPipeline(registry, text, &manager.root);
23 }
24
25 pub fn parseOpPassPipeline(
26 registry: *const registry_mod.PassRegistry,
27 text: []const u8,
28 manager: *pass_mod.OpPassManager,
29 ) anyerror!void {
30 var parsed = pass_mod.OpPassManager.initWithTarget(
31 manager.allocator,
32 manager.target_kind,
33 manager.target_op_name,
34 );
35 defer parsed.deinit();
36
37 var parser = Parser{
38 .registry = registry,
39 .text = text,
40 };
41 try parser.parsePipeline(&parsed, null);
42 try parser.expectDone();
43 try appendParsedManager(manager, &parsed);
44 }
45
46 pub fn formatPassManagerPipelineAlloc(
47 allocator: std.mem.Allocator,
48 manager: *const pass_mod.PassManager,
49 ) ![]u8 {
50 return try formatOpPassManagerPipelineAlloc(allocator, &manager.root);
51 }
52
53 pub fn formatOpPassManagerPipelineAlloc(
54 allocator: std.mem.Allocator,
55 manager: *const pass_mod.OpPassManager,
56 ) ![]u8 {
57 var out = std.Io.Writer.Allocating.init(allocator);
58 defer out.deinit();
59 try writeOpPassManagerPipeline(&out.writer, manager);
60 return try out.toOwnedSlice();
61 }
62
63 pub fn writePassManagerPipeline(
64 writer: *std.Io.Writer,
65 manager: *const pass_mod.PassManager,
66 ) std.Io.Writer.Error!void {
67 try writeOpPassManagerPipeline(writer, &manager.root);
68 }
69
70 pub fn writeOpPassManagerPipeline(
71 writer: *std.Io.Writer,
72 manager: *const pass_mod.OpPassManager,
73 ) std.Io.Writer.Error!void {
74 switch (manager.target_kind) {
75 .root => {},
76 .any => {
77 try writer.writeAll("any(");
78 },
79 .op => {
80 try writer.writeAll(manager.target_op_name.?);
81 try writer.writeByte('(');
82 },
83 }
84
85 for (manager.pipeline.items, 0..) |entry, index| {
86 if (index != 0) try writer.writeByte(',');
87 switch (entry) {
88 .pass => |pass| {
89 try writer.writeAll(pass.name);
90 if (pass.textual_options) |options| {
91 try writer.writeByte('{');
92 try writer.writeAll(options);
93 try writer.writeByte('}');
94 }
95 },
96 .nested => |nested| try writeOpPassManagerPipeline(writer, nested),
97 }
98 }
99
100 switch (manager.target_kind) {
101 .root => {},
102 .any, .op => try writer.writeByte(')'),
103 }
104 }
105
106 const Parser = struct {
107 registry: *const registry_mod.PassRegistry,
108 text: []const u8,
109 index: usize = 0,
110
111 fn parsePipeline(
112 self: *Parser,
113 manager: *pass_mod.OpPassManager,
114 close: ?u8,
115 ) anyerror!void {
116 self.skipWhitespace();
117 if (close) |close_char| {
118 if (self.consume(close_char)) return;
119 if (self.atEnd()) return TextualPipelineError.MissingCloseParen;
120 } else if (self.atEnd()) {
121 return;
122 } else if (self.peek() == ')') {
123 return TextualPipelineError.UnexpectedCloseParen;
124 }
125
126 while (true) {
127 if (self.atEnd()) {
128 if (close != null) return TextualPipelineError.MissingCloseParen;
129 return;
130 }
131
132 self.skipWhitespace();
133 if (self.atEnd()) return TextualPipelineError.EmptyElement;
134 if (self.peek() == ')') return TextualPipelineError.UnexpectedCloseParen;
135
136 const start = self.index;
137 while (!self.atEnd() and !isNameTerminator(self.peek())) {
138 self.index += 1;
139 }
140
141 const name = std.mem.trim(u8, self.text[start..self.index], " \t\r\n");
142 if (name.len == 0) return TextualPipelineError.EmptyElement;
143
144 self.skipWhitespace();
145 var option_block: ?ParsedOptionBlock = null;
146 defer if (option_block) |*block| block.deinit(manager.allocator);
147 if (self.consume('{')) {
148 option_block = try self.parseOptionBlock(manager.allocator);
149 self.skipWhitespace();
150 }
151 if (self.consume('(')) {
152 if (option_block != null) return TextualPipelineError.ExpectedComma;
153 const nested = if (std.mem.eql(u8, name, "any"))
154 try manager.nestAny()
155 else
156 try manager.nest(name);
157 try self.parsePipeline(nested, ')');
158 } else if (self.registry.lookupPipeline(name)) |registration| {
159 if (option_block) |block| {
160 try registration.addToWithOptions(manager, .{ .assignments = block.assignments });
161 } else {
162 try registration.addTo(manager);
163 }
164 } else if (self.registry.lookupPass(name)) |registration| {
165 if (option_block) |block| {
166 try registration.addToWithOptions(
167 manager,
168 block.text,
169 .{ .assignments = block.assignments },
170 );
171 } else {
172 try registration.addTo(manager);
173 }
174 } else {
175 return TextualPipelineError.UnknownPassOrPipeline;
176 }
177 self.skipWhitespace();
178
179 if (self.consume(',')) {
180 self.skipWhitespace();
181 if (self.atEnd()) return TextualPipelineError.EmptyElement;
182 if (close) |close_char| {
183 if (self.peek() == close_char) return TextualPipelineError.EmptyElement;
184 } else if (self.peek() == ')') {
185 return TextualPipelineError.UnexpectedCloseParen;
186 }
187 continue;
188 }
189
190 if (close) |close_char| {
191 if (self.consume(close_char)) return;
192 if (self.atEnd()) return TextualPipelineError.MissingCloseParen;
193 } else if (self.atEnd()) {
194 return;
195 } else if (self.peek() == ')') {
196 return TextualPipelineError.UnexpectedCloseParen;
197 }
198
199 return TextualPipelineError.ExpectedComma;
200 }
201 }
202
203 fn parseOptionBlock(self: *Parser, allocator: std.mem.Allocator) anyerror!ParsedOptionBlock {
204 const text_start = self.index;
205 var assignments: std.ArrayListUnmanaged(registry_mod.PassOptionAssignment) = .empty;
206 errdefer assignments.deinit(allocator);
207
208 while (true) {
209 self.skipWhitespace();
210 if (self.atEnd()) return TextualPipelineError.MissingCloseBrace;
211 if (self.consume('}')) {
212 return .{
213 .text = self.text[text_start .. self.index - 1],
214 .assignments = try assignments.toOwnedSlice(allocator),
215 };
216 }
217
218 const name_start = self.index;
219 while (!self.atEnd() and isOptionNameChar(self.peek())) {
220 self.index += 1;
221 }
222 const option_name = self.text[name_start..self.index];
223 if (option_name.len == 0) return TextualPipelineError.ExpectedPassOptionName;
224
225 self.skipWhitespace();
226 const option_value = if (self.consume('=')) value: {
227 self.skipWhitespace();
228 break :value try self.parseOptionValue();
229 } else "";
230 try assignments.append(allocator, .{
231 .name = option_name,
232 .value = option_value,
233 });
234
235 self.skipWhitespace();
236 if (self.consume(',')) continue;
237 if (self.consume('}')) {
238 return .{
239 .text = self.text[text_start .. self.index - 1],
240 .assignments = try assignments.toOwnedSlice(allocator),
241 };
242 }
243 }
244 }
245
246 fn parseOptionValue(self: *Parser) TextualPipelineError![]const u8 {
247 if (self.atEnd()) return TextualPipelineError.ExpectedPassOptionValue;
248 if (self.consume('"')) {
249 const start = self.index;
250 while (!self.atEnd() and self.peek() != '"') {
251 self.index += 1;
252 }
253 if (self.atEnd()) return TextualPipelineError.ExpectedPassOptionValue;
254 const value = self.text[start..self.index];
255 self.index += 1;
256 if (value.len == 0) return TextualPipelineError.ExpectedPassOptionValue;
257 return value;
258 }
259
260 const start = self.index;
261 while (!self.atEnd() and !isOptionValueTerminator(self.peek())) {
262 self.index += 1;
263 }
264 const value = self.text[start..self.index];
265 if (value.len == 0) return TextualPipelineError.ExpectedPassOptionValue;
266 return value;
267 }
268
269 fn expectDone(self: *Parser) TextualPipelineError!void {
270 self.skipWhitespace();
271 if (self.atEnd()) return;
272 if (self.peek() == ')') return TextualPipelineError.UnexpectedCloseParen;
273 return TextualPipelineError.ExpectedComma;
274 }
275
276 fn skipWhitespace(self: *Parser) void {
277 while (!self.atEnd() and std.ascii.isWhitespace(self.text[self.index])) {
278 self.index += 1;
279 }
280 }
281
282 fn consume(self: *Parser, ch: u8) bool {
283 if (self.atEnd() or self.text[self.index] != ch) return false;
284 self.index += 1;
285 return true;
286 }
287
288 fn peek(self: *const Parser) u8 {
289 return self.text[self.index];
290 }
291
292 fn atEnd(self: *const Parser) bool {
293 return self.index >= self.text.len;
294 }
295 };
296
297 fn appendParsedManager(
298 dst: *pass_mod.OpPassManager,
299 src: *pass_mod.OpPassManager,
300 ) std.mem.Allocator.Error!void {
301 try dst.pipeline.ensureUnusedCapacity(dst.allocator, src.pipeline.items.len);
302 try dst.nested_managers.ensureUnusedCapacity(dst.allocator, src.nested_managers.items.len);
303
304 for (src.nested_managers.items) |nested| {
305 nested.parent = dst;
306 }
307 dst.pipeline.appendSliceAssumeCapacity(src.pipeline.items);
308 dst.nested_managers.appendSliceAssumeCapacity(src.nested_managers.items);
309
310 src.pipeline.clearRetainingCapacity();
311 src.nested_managers.clearRetainingCapacity();
312 }
313
314 fn isNameTerminator(ch: u8) bool {
315 return ch == ',' or ch == '(' or ch == ')' or ch == '{' or ch == '}' or std.ascii.isWhitespace(ch);
316 }
317
318 fn isOptionNameChar(ch: u8) bool {
319 return std.ascii.isAlphanumeric(ch) or ch == '_' or ch == '-';
320 }
321
322 fn isOptionValueTerminator(ch: u8) bool {
323 return ch == ',' or ch == '}' or std.ascii.isWhitespace(ch);
324 }
325
326 const ParsedOptionBlock = struct {
327 text: []const u8,
328 assignments: []registry_mod.PassOptionAssignment,
329
330 fn deinit(self: *ParsedOptionBlock, allocator: std.mem.Allocator) void {
331 allocator.free(self.assignments);
332 }
333 };
334
335 fn noopPass(_: *pass_mod.PassContext) pass_mod.PassResult {
336 return .success;
337 }
338
339 const textual_test_pass = pass_mod.Pass{
340 .name = "choir-textual-test-pass",
341 .description = "test pass for textual pipeline parsing",
342 .run_fn = noopPass,
343 };
344
345 const textual_test_pass_registration = registry_mod.PassRegistration{
346 .name = textual_test_pass.name,
347 .description = textual_test_pass.description,
348 .pass = textual_test_pass,
349 };
350
351 const textual_option_choices = [_]registry_mod.PassOptionChoice{
352 .{ .name = "fast" },
353 .{ .name = "slow" },
354 };
355
356 const textual_test_option_specs = [_]registry_mod.PassOptionSpec{
357 .{
358 .name = "mode",
359 .description = "test mode",
360 .kind = .choice,
361 .choices = &textual_option_choices,
362 .default_value = "fast",
363 },
364 .{
365 .name = "limit",
366 .description = "test limit",
367 .kind = .unsigned,
368 .default_value = "0",
369 },
370 .{
371 .name = "enabled",
372 .description = "test toggle",
373 .kind = .boolean,
374 .default_value = "false",
375 },
376 };
377
378 const textual_test_option_pass = pass_mod.Pass{
379 .name = "choir-textual-test-option-pass",
380 .description = "test pass for textual pipeline options",
381 .run_fn = noopPass,
382 };
383
384 fn buildTextualTestOptionPass(_: std.mem.Allocator, options: registry_mod.PassOptionSet) anyerror!pass_mod.Pass {
385 _ = options.choiceValue("mode", "fast");
386 _ = try options.unsignedValue(usize, "limit", 0);
387 _ = try options.boolValue("enabled", false);
388 return textual_test_option_pass;
389 }
390
391 const textual_test_option_pass_registration = registry_mod.PassRegistration{
392 .name = textual_test_option_pass.name,
393 .description = textual_test_option_pass.description,
394 .pass = textual_test_option_pass,
395 .options = &textual_test_option_specs,
396 .build_with_options = buildTextualTestOptionPass,
397 };
398
399 fn buildTextualTestPipeline(manager: *pass_mod.OpPassManager) anyerror!void {
400 try manager.addPass(textual_test_pass);
401 }
402
403 const textual_test_pipeline = registry_mod.PipelineRegistration{
404 .name = "choir-textual-test-pipeline",
405 .description = "test textual pipeline registration",
406 .build = buildTextualTestPipeline,
407 };
408
409 const textual_pipeline_option_specs = [_]registry_mod.PassOptionSpec{
410 .{
411 .name = "limit",
412 .description = "test pipeline limit",
413 .kind = .unsigned,
414 .default_value = "0",
415 },
416 .{
417 .name = "enabled",
418 .description = "test pipeline toggle",
419 .kind = .boolean,
420 .default_value = "false",
421 },
422 };
423
424 fn buildTextualTestPipelineWithOptions(
425 manager: *pass_mod.OpPassManager,
426 options: registry_mod.PassOptionSet,
427 ) anyerror!void {
428 _ = try options.boolValue("enabled", false);
429 const limit = options.get("limit") orelse "0";
430 const option_text = try std.fmt.allocPrint(manager.allocator, "limit={s}", .{limit});
431 defer manager.allocator.free(option_text);
432 try textual_test_option_pass_registration.addToWithOptions(
433 manager,
434 option_text,
435 options,
436 );
437 }
438
439 const textual_test_option_pipeline = registry_mod.PipelineRegistration{
440 .name = "choir-textual-test-option-pipeline",
441 .description = "test textual pipeline options",
442 .build = buildTextualTestPipeline,
443 .options = &textual_pipeline_option_specs,
444 .build_with_options = buildTextualTestPipelineWithOptions,
445 };
446
447 fn buildTextualRegistry(allocator: std.mem.Allocator) !registry_mod.PassRegistry {
448 var registry = registry_mod.PassRegistry.init(allocator);
449 errdefer registry.deinit();
450 try registry.registerPass(textual_test_pass_registration);
451 try registry.registerPass(textual_test_option_pass_registration);
452 try registry.registerPipeline(textual_test_pipeline);
453 try registry.registerPipeline(textual_test_option_pipeline);
454 return registry;
455 }
456
457 test "textual pipeline parses registered passes and pipelines" {
458 var registry = try buildTextualRegistry(std.testing.allocator);
459 defer registry.deinit();
460
461 var manager = pass_mod.PassManager.init(std.testing.allocator);
462 defer manager.deinit();
463 try parsePassPipeline(®istry, " choir-textual-test-pass , choir-textual-test-pipeline ", &manager);
464
465 try std.testing.expectEqual(@as(usize, 2), manager.root.pipeline.items.len);
466
467 const text = try formatPassManagerPipelineAlloc(std.testing.allocator, &manager);
468 defer std.testing.allocator.free(text);
469 try std.testing.expectEqualStrings("choir-textual-test-pass,choir-textual-test-pass", text);
470 }
471
472 test "textual pipeline parses explicit operation nesting" {
473 var registry = try buildTextualRegistry(std.testing.allocator);
474 defer registry.deinit();
475
476 var manager = pass_mod.PassManager.init(std.testing.allocator);
477 defer manager.deinit();
478 try parsePassPipeline(®istry, "test.op(choir-textual-test-pass)", &manager);
479
480 try std.testing.expectEqual(@as(usize, 1), manager.root.pipeline.items.len);
481 switch (manager.root.pipeline.items[0]) {
482 .nested => |nested| {
483 try std.testing.expectEqualStrings("test.op", nested.target_op_name.?);
484 try std.testing.expectEqual(@as(usize, 1), nested.pipeline.items.len);
485 },
486 .pass => return error.TestExpectedNestedPassManager,
487 }
488
489 const text = try formatPassManagerPipelineAlloc(std.testing.allocator, &manager);
490 defer std.testing.allocator.free(text);
491 try std.testing.expectEqualStrings("test.op(choir-textual-test-pass)", text);
492 }
493
494 test "textual pipeline parses op-agnostic any nesting" {
495 var registry = try buildTextualRegistry(std.testing.allocator);
496 defer registry.deinit();
497
498 var manager = pass_mod.PassManager.init(std.testing.allocator);
499 defer manager.deinit();
500 try parsePassPipeline(®istry, "any(choir-textual-test-pass)", &manager);
501
502 try std.testing.expectEqual(@as(usize, 1), manager.root.pipeline.items.len);
503 switch (manager.root.pipeline.items[0]) {
504 .nested => |nested| {
505 try std.testing.expectEqual(pass_mod.OpPassManagerTargetKind.any, nested.target_kind);
506 try std.testing.expectEqual(@as(?[]const u8, null), nested.target_op_name);
507 try std.testing.expectEqual(@as(usize, 1), nested.pipeline.items.len);
508 },
509 .pass => return error.TestExpectedNestedPassManager,
510 }
511
512 const text = try formatPassManagerPipelineAlloc(std.testing.allocator, &manager);
513 defer std.testing.allocator.free(text);
514 try std.testing.expectEqualStrings("any(choir-textual-test-pass)", text);
515 }
516
517 test "textual pipeline parses pass options and formats them" {
518 var registry = try buildTextualRegistry(std.testing.allocator);
519 defer registry.deinit();
520
521 var manager = pass_mod.PassManager.init(std.testing.allocator);
522 defer manager.deinit();
523 try parsePassPipeline(
524 ®istry,
525 "choir-textual-test-option-pass{mode=slow,limit=7}",
526 &manager,
527 );
528
529 try std.testing.expectEqual(@as(usize, 1), manager.root.pipeline.items.len);
530 switch (manager.root.pipeline.items[0]) {
531 .pass => |pass| {
532 try std.testing.expectEqualStrings(textual_test_option_pass.name, pass.name);
533 try std.testing.expectEqualStrings("mode=slow,limit=7", pass.textual_options.?);
534 },
535 .nested => return error.TestExpectedPass,
536 }
537
538 const text = try formatPassManagerPipelineAlloc(std.testing.allocator, &manager);
539 defer std.testing.allocator.free(text);
540 try std.testing.expectEqualStrings("choir-textual-test-option-pass{mode=slow,limit=7}", text);
541 }
542
543 test "textual pipeline parses MLIR-style boolean pass option values" {
544 var registry = try buildTextualRegistry(std.testing.allocator);
545 defer registry.deinit();
546
547 var manager = pass_mod.PassManager.init(std.testing.allocator);
548 defer manager.deinit();
549 try parsePassPipeline(
550 ®istry,
551 "choir-textual-test-option-pass{enabled,limit=7},choir-textual-test-option-pass{enabled=1},choir-textual-test-option-pass{enabled=0}",
552 &manager,
553 );
554
555 try std.testing.expectEqual(@as(usize, 3), manager.root.pipeline.items.len);
556 switch (manager.root.pipeline.items[0]) {
557 .pass => |pass| try std.testing.expectEqualStrings("enabled,limit=7", pass.textual_options.?),
558 .nested => return error.TestExpectedPass,
559 }
560 switch (manager.root.pipeline.items[1]) {
561 .pass => |pass| try std.testing.expectEqualStrings("enabled=1", pass.textual_options.?),
562 .nested => return error.TestExpectedPass,
563 }
564 switch (manager.root.pipeline.items[2]) {
565 .pass => |pass| try std.testing.expectEqualStrings("enabled=0", pass.textual_options.?),
566 .nested => return error.TestExpectedPass,
567 }
568 }
569
570 test "textual pipeline parses pass options inside nested managers" {
571 var registry = try buildTextualRegistry(std.testing.allocator);
572 defer registry.deinit();
573
574 var manager = pass_mod.PassManager.init(std.testing.allocator);
575 defer manager.deinit();
576 try parsePassPipeline(
577 ®istry,
578 "test.op(choir-textual-test-option-pass{mode=slow limit=11})",
579 &manager,
580 );
581
582 const text = try formatPassManagerPipelineAlloc(std.testing.allocator, &manager);
583 defer std.testing.allocator.free(text);
584 try std.testing.expectEqualStrings("test.op(choir-textual-test-option-pass{mode=slow limit=11})", text);
585 }
586
587 test "textual pipeline parses registered pipeline options" {
588 var registry = try buildTextualRegistry(std.testing.allocator);
589 defer registry.deinit();
590
591 var manager = pass_mod.PassManager.init(std.testing.allocator);
592 defer manager.deinit();
593 try parsePassPipeline(
594 ®istry,
595 "choir-textual-test-option-pipeline{limit=13}",
596 &manager,
597 );
598
599 try std.testing.expectEqual(@as(usize, 1), manager.root.pipeline.items.len);
600 switch (manager.root.pipeline.items[0]) {
601 .pass => |pass| {
602 try std.testing.expectEqualStrings(textual_test_option_pass.name, pass.name);
603 try std.testing.expectEqualStrings("limit=13", pass.textual_options.?);
604 },
605 .nested => return error.TestExpectedPass,
606 }
607
608 const text = try formatPassManagerPipelineAlloc(std.testing.allocator, &manager);
609 defer std.testing.allocator.free(text);
610 try std.testing.expectEqualStrings("choir-textual-test-option-pass{limit=13}", text);
611 }
612
613 test "textual pipeline validates declared pass options" {
614 var registry = try buildTextualRegistry(std.testing.allocator);
615 defer registry.deinit();
616
617 var manager = pass_mod.PassManager.init(std.testing.allocator);
618 defer manager.deinit();
619
620 try std.testing.expectError(
621 error.UnknownPassOption,
622 parsePassPipeline(®istry, "choir-textual-test-option-pass{missing=1}", &manager),
623 );
624 try std.testing.expectError(
625 error.DuplicatePassOption,
626 parsePassPipeline(®istry, "choir-textual-test-option-pass{limit=1,limit=2}", &manager),
627 );
628 try std.testing.expectError(
629 error.InvalidPassOptionValue,
630 parsePassPipeline(®istry, "choir-textual-test-option-pass{mode=medium}", &manager),
631 );
632 try std.testing.expectError(
633 error.PassOptionsNotSupported,
634 parsePassPipeline(®istry, "choir-textual-test-pass{limit=1}", &manager),
635 );
636 try std.testing.expectError(
637 error.UnknownPassOption,
638 parsePassPipeline(®istry, "choir-textual-test-option-pipeline{missing=1}", &manager),
639 );
640 try std.testing.expectError(
641 error.InvalidPassOptionValue,
642 parsePassPipeline(®istry, "choir-textual-test-option-pipeline{limit=nope}", &manager),
643 );
644 try std.testing.expectError(
645 error.MissingPassOptionValue,
646 parsePassPipeline(®istry, "choir-textual-test-option-pipeline{limit}", &manager),
647 );
648 try std.testing.expectError(
649 error.PassOptionsNotSupported,
650 parsePassPipeline(®istry, "choir-textual-test-pipeline{limit=1}", &manager),
651 );
652 }
653
654 test "textual pipeline accepts empty root and nested pipelines" {
655 var registry = try buildTextualRegistry(std.testing.allocator);
656 defer registry.deinit();
657
658 var manager = pass_mod.PassManager.init(std.testing.allocator);
659 defer manager.deinit();
660 try parsePassPipeline(®istry, " \t\n ", &manager);
661 try std.testing.expectEqual(@as(usize, 0), manager.root.pipeline.items.len);
662
663 try parsePassPipeline(®istry, "test.empty()", &manager);
664 const text = try formatPassManagerPipelineAlloc(std.testing.allocator, &manager);
665 defer std.testing.allocator.free(text);
666 try std.testing.expectEqualStrings("test.empty()", text);
667 }
668
669 test "textual pipeline parse errors do not mutate the destination manager" {
670 var registry = try buildTextualRegistry(std.testing.allocator);
671 defer registry.deinit();
672
673 var manager = pass_mod.PassManager.init(std.testing.allocator);
674 defer manager.deinit();
675 try parsePassPipeline(®istry, "choir-textual-test-pass", &manager);
676 try std.testing.expectEqual(@as(usize, 1), manager.root.pipeline.items.len);
677
678 try std.testing.expectError(
679 TextualPipelineError.UnknownPassOrPipeline,
680 parsePassPipeline(®istry, "choir-textual-test-pipeline,missing-pass", &manager),
681 );
682 try std.testing.expectEqual(@as(usize, 1), manager.root.pipeline.items.len);
683 }
684
685 test "textual pipeline reports malformed nesting" {
686 var registry = try buildTextualRegistry(std.testing.allocator);
687 defer registry.deinit();
688
689 var manager = pass_mod.PassManager.init(std.testing.allocator);
690 defer manager.deinit();
691
692 try std.testing.expectError(
693 TextualPipelineError.MissingCloseParen,
694 parsePassPipeline(®istry, "test.op(choir-textual-test-pass", &manager),
695 );
696 try std.testing.expectError(
697 TextualPipelineError.UnexpectedCloseParen,
698 parsePassPipeline(®istry, "choir-textual-test-pass)", &manager),
699 );
700 try std.testing.expectError(
701 TextualPipelineError.EmptyElement,
702 parsePassPipeline(®istry, "choir-textual-test-pass,", &manager),
703 );
704 }