lib/choir/src/passes/options.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 pub const PassOptionError = error{
4 DuplicatePassOption,
5 InvalidPassOptionValue,
6 MissingPassOptionValue,
7 UnknownPassOption,
8 };
9
10 pub const PassOptionKind = enum {
11 boolean,
12 unsigned,
13 string,
14 choice,
15 };
16
17 pub const PassOptionChoice = struct {
18 name: []const u8,
19 description: []const u8 = "",
20 };
21
22 pub const PassOptionSpec = struct {
23 name: []const u8,
24 description: []const u8,
25 kind: PassOptionKind,
26 choices: []const PassOptionChoice = &.{},
27 default_value: ?[]const u8 = null,
28
29 pub fn validate(self: PassOptionSpec, value: []const u8) PassOptionError!void {
30 switch (self.kind) {
31 .boolean => _ = parseBoolValue(value) catch return error.InvalidPassOptionValue,
32 .unsigned => {
33 if (value.len == 0) return error.MissingPassOptionValue;
34 _ = std.fmt.parseUnsigned(usize, value, 10) catch return error.InvalidPassOptionValue;
35 },
36 .string => {
37 if (value.len == 0) return error.MissingPassOptionValue;
38 },
39 .choice => {
40 if (value.len == 0) return error.MissingPassOptionValue;
41 for (self.choices) |choice| {
42 if (std.mem.eql(u8, choice.name, value)) return;
43 }
44 return error.InvalidPassOptionValue;
45 },
46 }
47 }
48 };
49
50 pub const PassOptionAssignment = struct {
51 name: []const u8,
52 value: []const u8,
53 };
54
55 pub const PassOptionSet = struct {
56 assignments: []const PassOptionAssignment = &.{},
57
58 pub fn get(self: PassOptionSet, name: []const u8) ?[]const u8 {
59 for (self.assignments) |assignment| {
60 if (std.mem.eql(u8, assignment.name, name)) return assignment.value;
61 }
62 return null;
63 }
64
65 pub fn boolValue(self: PassOptionSet, name: []const u8, default: bool) PassOptionError!bool {
66 const value = self.get(name) orelse return default;
67 return parseBoolValue(value) catch return error.InvalidPassOptionValue;
68 }
69
70 pub fn unsignedValue(
71 self: PassOptionSet,
72 comptime T: type,
73 name: []const u8,
74 default: T,
75 ) PassOptionError!T {
76 const value = self.get(name) orelse return default;
77 return std.fmt.parseUnsigned(T, value, 10) catch return error.InvalidPassOptionValue;
78 }
79
80 pub fn choiceValue(self: PassOptionSet, name: []const u8, default: []const u8) []const u8 {
81 return self.get(name) orelse default;
82 }
83 };
84
85 pub fn validatePassOptions(
86 specs: []const PassOptionSpec,
87 set: PassOptionSet,
88 ) PassOptionError!void {
89 for (set.assignments, 0..) |assignment, index| {
90 const spec = lookupSpec(specs, assignment.name) orelse return error.UnknownPassOption;
91 for (set.assignments[0..index]) |prior| {
92 if (std.mem.eql(u8, prior.name, assignment.name)) return error.DuplicatePassOption;
93 }
94 try spec.validate(assignment.value);
95 }
96 }
97
98 fn lookupSpec(specs: []const PassOptionSpec, name: []const u8) ?PassOptionSpec {
99 for (specs) |spec| {
100 if (std.mem.eql(u8, spec.name, name)) return spec;
101 }
102 return null;
103 }
104
105 fn parseBoolValue(value: []const u8) !bool {
106 if (value.len == 0) return true;
107 if (std.mem.eql(u8, value, "true")) return true;
108 if (std.mem.eql(u8, value, "false")) return false;
109 if (std.mem.eql(u8, value, "1")) return true;
110 if (std.mem.eql(u8, value, "0")) return false;
111 return error.InvalidBoolean;
112 }
113
114 test "pass option set validates declared choices and unsigned values" {
115 const choices = [_]PassOptionChoice{
116 .{ .name = "auto" },
117 .{ .name = "beam" },
118 };
119 const specs = [_]PassOptionSpec{
120 .{
121 .name = "strategy",
122 .description = "planning strategy",
123 .kind = .choice,
124 .choices = &choices,
125 },
126 .{
127 .name = "beam-width",
128 .description = "beam width",
129 .kind = .unsigned,
130 },
131 };
132 const assignments = [_]PassOptionAssignment{
133 .{ .name = "strategy", .value = "beam" },
134 .{ .name = "beam-width", .value = "32" },
135 };
136 const set = PassOptionSet{ .assignments = &assignments };
137
138 try validatePassOptions(&specs, set);
139 try std.testing.expectEqualStrings("beam", set.choiceValue("strategy", "auto"));
140 try std.testing.expectEqual(@as(usize, 32), try set.unsignedValue(usize, "beam-width", 64));
141 }
142
143 test "pass option set rejects unknown duplicate and invalid values" {
144 const specs = [_]PassOptionSpec{
145 .{
146 .name = "enabled",
147 .description = "toggle",
148 .kind = .boolean,
149 },
150 };
151 try std.testing.expectError(
152 error.UnknownPassOption,
153 validatePassOptions(&specs, .{ .assignments = &.{.{ .name = "missing", .value = "true" }} }),
154 );
155 try std.testing.expectError(
156 error.DuplicatePassOption,
157 validatePassOptions(&specs, .{ .assignments = &.{
158 .{ .name = "enabled", .value = "true" },
159 .{ .name = "enabled", .value = "false" },
160 } }),
161 );
162 try std.testing.expectError(
163 error.InvalidPassOptionValue,
164 validatePassOptions(&specs, .{ .assignments = &.{.{ .name = "enabled", .value = "yes" }} }),
165 );
166 }
167
168 test "pass option booleans accept MLIR-style flag and numeric spellings" {
169 const specs = [_]PassOptionSpec{
170 .{
171 .name = "enabled",
172 .description = "toggle",
173 .kind = .boolean,
174 },
175 .{
176 .name = "limit",
177 .description = "limit",
178 .kind = .unsigned,
179 },
180 };
181
182 const enabled_flag = PassOptionSet{ .assignments = &.{.{ .name = "enabled", .value = "" }} };
183 try validatePassOptions(&specs, enabled_flag);
184 try std.testing.expect(try enabled_flag.boolValue("enabled", false));
185
186 const enabled_numeric = PassOptionSet{ .assignments = &.{.{ .name = "enabled", .value = "1" }} };
187 try validatePassOptions(&specs, enabled_numeric);
188 try std.testing.expect(try enabled_numeric.boolValue("enabled", false));
189
190 const disabled_numeric = PassOptionSet{ .assignments = &.{.{ .name = "enabled", .value = "0" }} };
191 try validatePassOptions(&specs, disabled_numeric);
192 try std.testing.expect(!try disabled_numeric.boolValue("enabled", true));
193
194 try std.testing.expectError(
195 error.MissingPassOptionValue,
196 validatePassOptions(&specs, .{ .assignments = &.{.{ .name = "limit", .value = "" }} }),
197 );
198 }