a73x

d5baa40e

refactor: one sum owns "name one transport"

a73x   2026-08-27 08:12

Commit message
refactor: one sum owns "name one transport"

The rule was written twice: positional latched a bool when it saw a
second host or a second quic://, and parseArgs summed the four
spellings. Positionals are now counted, so both readings of two
transports are the same arithmetic in one place.

src/cli/mux_main.zig
Old New
@@ -184,24 +184,21 @@ const Opts = struct {
184 /// says so: they are what `positional` saw. 184 /// says so: they are what `positional` saw.
185 _host: ?[]const u8 = null, 185 _host: ?[]const u8 = null,
186 _quic: ?[]const u8 = null, 186 _quic: ?[]const u8 = null,
187 _conflict: bool = false, 187 _targets: u8 = 0,
188 188
189 pub const aliases = .{.{ "-A", "agent" }}; 189 pub const aliases = .{.{ "-A", "agent" }};
190 190
191 /// A bare word is a host to hop to, `quic://...` a transport spelling. 191 /// A bare word is a host to hop to, `quic://...` a transport spelling.
192 /// A second of either is as ambiguous as naming two transports, so it 192 /// Counted, not judged: parseArgs owns the one sum that refuses two of
193 /// lands in the same refusal; `quic://` with nothing after it names no 193 /// anything. `quic://` with nothing after it names no host at all, and
194 /// host at all and is refused as the usage mistake it is. 194 /// is refused here as the usage mistake it is.
195 pub fn positional(self: *Opts, word: []const u8) bool { 195 pub fn positional(self: *Opts, word: []const u8) bool {
196 if (std.mem.startsWith(u8, word, "quic://")) { 196 if (std.mem.startsWith(u8, word, "quic://")) {
197 const host_port = word["quic://".len..]; 197 const host_port = word["quic://".len..];
198 if (host_port.len == 0) return false; 198 if (host_port.len == 0) return false;
199 if (self._quic != null) self._conflict = true;
200 self._quic = host_port; 199 self._quic = host_port;
201 return true; 200 } else self._host = word;
202 } 201 self._targets += 1;
203 if (self._host != null) self._conflict = true;
204 self._host = word;
205 return true; 202 return true;
206 } 203 }
207 }; 204 };
@@ -219,12 +216,10 @@ fn parseArgs(args: []const [:0]const u8, env_key: ?[]const u8) ParseError!ParseR
219 .unknown_arg, .missing_value, .bad_value => return error.Usage, 216 .unknown_arg, .missing_value, .bad_value => return error.Usage,
220 } 217 }
221 218
222 if (o._conflict) return error.Conflict; 219 // Every pairing is two transports for one session, and two bare words
223 220 // are a pairing too — which is why positional counts, not latches.
224 // Every pairing of the four is two transports for one session.
225 const named: u8 = @as(u8, @intFromBool(o.sock != null)) + 221 const named: u8 = @as(u8, @intFromBool(o.sock != null)) +
226 @intFromBool(o.via != null) + @intFromBool(o._host != null) + 222 @intFromBool(o.via != null) + o._targets;
227 @intFromBool(o._quic != null);
228 if (named > 1) return error.Conflict; 223 if (named > 1) return error.Conflict;
229 224
230 // Rides every transport below, unlike --key: a session name is not 225 // Rides every transport below, unlike --key: a session name is not