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 }