Commit 36bade5c56

Andrew Kelley <andrew@ziglang.org>
2019-02-07 06:42:41
fixups, and modify std.mem.join and std.os.path.resolve API
* zig fmt * std.mem.join takes a slice of slices instead of var args * std.mem.join takes a separator slice rather than byte, and always inserts it. Previously it would not insert the separator if there already was one, violating the documented behavior. * std.mem.join calculates exactly the correct amount to allocate and has no call to allocator.shrink() * bring back joinWindows and joinPosix and the corresponding tests. it is intended to be able to call these functions from any OS. * rename std.os.path.resolveSlice to resolve (now resolve takes a slice of slices instead of var args)
1 parent c804ae2
doc/docgen.zig
@@ -990,13 +990,19 @@ fn genHtml(allocator: *mem.Allocator, tokenizer: *Tokenizer, toc: *Toc, out: var
                 try tokenizeAndPrint(tokenizer, out, code.source_token);
                 try out.write("</pre>");
                 const name_plus_ext = try std.fmt.allocPrint(allocator, "{}.zig", code.name);
-                const tmp_source_file_name = try os.path.join(allocator, [][]const u8{ tmp_dir_name, name_plus_ext });
+                const tmp_source_file_name = try os.path.join(
+                    allocator,
+                    [][]const u8{ tmp_dir_name, name_plus_ext },
+                );
                 try io.writeFile(tmp_source_file_name, trimmed_raw_source);
 
                 switch (code.id) {
                     Code.Id.Exe => |expected_outcome| {
                         const name_plus_bin_ext = try std.fmt.allocPrint(allocator, "{}{}", code.name, exe_ext);
-                        const tmp_bin_file_name = try os.path.join(allocator, [][]const u8{ tmp_dir_name, name_plus_bin_ext });
+                        const tmp_bin_file_name = try os.path.join(
+                            allocator,
+                            [][]const u8{ tmp_dir_name, name_plus_bin_ext },
+                        );
                         var build_args = std.ArrayList([]const u8).init(allocator);
                         defer build_args.deinit();
                         try build_args.appendSlice([][]const u8{
@@ -1024,7 +1030,10 @@ fn genHtml(allocator: *mem.Allocator, tokenizer: *Tokenizer, toc: *Toc, out: var
                         }
                         for (code.link_objects) |link_object| {
                             const name_with_ext = try std.fmt.allocPrint(allocator, "{}{}", link_object, obj_ext);
-                            const full_path_object = try os.path.join(allocator, [][]const u8{ tmp_dir_name, name_with_ext });
+                            const full_path_object = try os.path.join(
+                                allocator,
+                                [][]const u8{ tmp_dir_name, name_with_ext },
+                            );
                             try build_args.append("--object");
                             try build_args.append(full_path_object);
                             try out.print(" --object {}", name_with_ext);
@@ -1216,12 +1225,18 @@ fn genHtml(allocator: *mem.Allocator, tokenizer: *Tokenizer, toc: *Toc, out: var
                     },
                     Code.Id.Obj => |maybe_error_match| {
                         const name_plus_obj_ext = try std.fmt.allocPrint(allocator, "{}{}", code.name, obj_ext);
-                        const tmp_obj_file_name = try os.path.join(allocator, [][]const u8{ tmp_dir_name, name_plus_obj_ext });
+                        const tmp_obj_file_name = try os.path.join(
+                            allocator,
+                            [][]const u8{ tmp_dir_name, name_plus_obj_ext },
+                        );
                         var build_args = std.ArrayList([]const u8).init(allocator);
                         defer build_args.deinit();
 
                         const name_plus_h_ext = try std.fmt.allocPrint(allocator, "{}.h", code.name);
-                        const output_h_file_name = try os.path.join(allocator, [][]const u8{ tmp_dir_name, name_plus_h_ext });
+                        const output_h_file_name = try os.path.join(
+                            allocator,
+                            [][]const u8{ tmp_dir_name, name_plus_h_ext },
+                        );
 
                         try build_args.appendSlice([][]const u8{
                             zig_exe,
src-self-hosted/libc_installation.zig
@@ -254,7 +254,10 @@ pub const LibCInstallation = struct {
             const stream = &std.io.BufferOutStream.init(&result_buf).stream;
             try stream.print("{}\\Include\\{}\\ucrt", search.path, search.version);
 
-            const stdlib_path = try std.os.path.join(loop.allocator, [][]const u8{ result_buf.toSliceConst(), "stdlib.h" });
+            const stdlib_path = try std.os.path.join(
+                loop.allocator,
+                [][]const u8{ result_buf.toSliceConst(), "stdlib.h" },
+            );
             defer loop.allocator.free(stdlib_path);
 
             if (try fileExists(stdlib_path)) {
@@ -283,7 +286,10 @@ pub const LibCInstallation = struct {
                 builtin.Arch.aarch64v8 => try stream.write("arm"),
                 else => return error.UnsupportedArchitecture,
             }
-            const ucrt_lib_path = try std.os.path.join(loop.allocator, [][]const u8{ result_buf.toSliceConst(), "ucrt.lib" });
+            const ucrt_lib_path = try std.os.path.join(
+                loop.allocator,
+                [][]const u8{ result_buf.toSliceConst(), "ucrt.lib" },
+            );
             defer loop.allocator.free(ucrt_lib_path);
             if (try fileExists(ucrt_lib_path)) {
                 self.lib_dir = result_buf.toOwnedSlice();
@@ -358,7 +364,10 @@ pub const LibCInstallation = struct {
                 builtin.Arch.aarch64v8 => try stream.write("arm\\"),
                 else => return error.UnsupportedArchitecture,
             }
-            const kernel32_path = try std.os.path.join(loop.allocator, [][]const u8{ result_buf.toSliceConst(), "kernel32.lib" });
+            const kernel32_path = try std.os.path.join(
+                loop.allocator,
+                [][]const u8{ result_buf.toSliceConst(), "kernel32.lib" },
+            );
             defer loop.allocator.free(kernel32_path);
             if (try fileExists(kernel32_path)) {
                 self.kernel32_lib_dir = result_buf.toOwnedSlice();
std/debug/index.zig
@@ -774,7 +774,7 @@ fn openSelfDebugInfoWindows(allocator: *mem.Allocator) !DebugInfo {
     const len = try di.coff.getPdbPath(path_buf[0..]);
     const raw_path = path_buf[0..len];
 
-    const path = try os.path.resolve(allocator, raw_path);
+    const path = try os.path.resolve(allocator, [][]const u8{raw_path});
 
     try di.pdb.openFile(di.coff, path);
 
@@ -1352,7 +1352,7 @@ const LineNumberProgram = struct {
                 return error.InvalidDebugInfo;
             } else
                 self.include_dirs[file_entry.dir_index];
-            const file_name = try os.path.join(self.file_entries.allocator, [][]const u8{dir_name, file_entry.file_name});
+            const file_name = try os.path.join(self.file_entries.allocator, [][]const u8{ dir_name, file_entry.file_name });
             errdefer self.file_entries.allocator.free(file_name);
             return LineInfo{
                 .line = if (self.prev_line >= 0) @intCast(usize, self.prev_line) else 0,
std/event/fs.zig
@@ -871,7 +871,7 @@ pub fn Watch(comptime V: type) type {
         }
 
         async fn addFileKEvent(self: *Self, file_path: []const u8, value: V) !?V {
-            const resolved_path = try os.path.resolve(self.channel.loop.allocator, file_path);
+            const resolved_path = try os.path.resolve(self.channel.loop.allocator, [][]const u8{file_path});
             var resolved_path_consumed = false;
             defer if (!resolved_path_consumed) self.channel.loop.allocator.free(resolved_path);
 
@@ -1336,7 +1336,7 @@ async fn testFsWatchCantFail(loop: *Loop, result: *(anyerror!void)) void {
 }
 
 async fn testFsWatch(loop: *Loop) !void {
-    const file_path = try os.path.join(loop.allocator, [][]const u8{test_tmp_dir, "file.txt"});
+    const file_path = try os.path.join(loop.allocator, [][]const u8{ test_tmp_dir, "file.txt" });
     defer loop.allocator.free(file_path);
 
     const contents =
std/os/child_process.zig
@@ -574,7 +574,7 @@ pub const ChildProcess = struct {
         // to match posix semantics
         const app_name = x: {
             if (self.cwd) |cwd| {
-                const resolved = try os.path.resolve(self.allocator, cwd, self.argv[0]);
+                const resolved = try os.path.resolve(self.allocator, [][]const u8{ cwd, self.argv[0] });
                 defer self.allocator.free(resolved);
                 break :x try cstr.addNullByte(self.allocator, resolved);
             } else {
std/os/get_app_data_dir.zig
@@ -30,7 +30,7 @@ pub fn getAppDataDir(allocator: *mem.Allocator, appname: []const u8) GetAppDataD
                         error.OutOfMemory => return error.OutOfMemory,
                     };
                     defer allocator.free(global_dir);
-                    return os.path.join(allocator, [][]const u8{global_dir, appname});
+                    return os.path.join(allocator, [][]const u8{ global_dir, appname });
                 },
                 os.windows.E_OUTOFMEMORY => return error.OutOfMemory,
                 else => return error.AppDataDirUnavailable,
@@ -41,14 +41,14 @@ pub fn getAppDataDir(allocator: *mem.Allocator, appname: []const u8) GetAppDataD
                 // TODO look in /etc/passwd
                 return error.AppDataDirUnavailable;
             };
-            return os.path.join(allocator, [][]const u8{home_dir, "Library", "Application Support", appname});
+            return os.path.join(allocator, [][]const u8{ home_dir, "Library", "Application Support", appname });
         },
         builtin.Os.linux, builtin.Os.freebsd => {
             const home_dir = os.getEnvPosix("HOME") orelse {
                 // TODO look in /etc/passwd
                 return error.AppDataDirUnavailable;
             };
-            return os.path.join(allocator, [][]const u8{home_dir, ".local", "share", appname});
+            return os.path.join(allocator, [][]const u8{ home_dir, ".local", "share", appname });
         },
         else => @compileError("Unsupported OS"),
     }
@@ -67,4 +67,3 @@ test "std.os.getAppDataDir" {
     // We can't actually validate the result
     _ = getAppDataDir(allocator, "zig") catch return;
 }
-
std/os/index.zig
@@ -1284,7 +1284,7 @@ pub fn makeDirPosix(dir_path: []const u8) !void {
 /// already exists and is a directory.
 /// TODO determine if we can remove the allocator requirement from this function
 pub fn makePath(allocator: *Allocator, full_path: []const u8) !void {
-    const resolved_path = try path.resolve(allocator, full_path);
+    const resolved_path = try path.resolve(allocator, [][]const u8{full_path});
     defer allocator.free(resolved_path);
 
     var end_index: usize = resolved_path.len;
@@ -2304,18 +2304,17 @@ pub fn selfExePath(out_buffer: *[MAX_PATH_BYTES]u8) ![]u8 {
     switch (builtin.os) {
         Os.linux => return readLink(out_buffer, "/proc/self/exe"),
         Os.freebsd => {
-            var mib = [4]c_int{ posix.CTL_KERN, posix.KERN_PROC, posix.KERN_PROC_PATHNAME, -1};
+            var mib = [4]c_int{ posix.CTL_KERN, posix.KERN_PROC, posix.KERN_PROC_PATHNAME, -1 };
             var out_len: usize = out_buffer.len;
             const err = posix.getErrno(posix.sysctl(&mib, 4, out_buffer, &out_len, null, 0));
 
-            if (err == 0 ) return mem.toSlice(u8, out_buffer);
+            if (err == 0) return mem.toSlice(u8, out_buffer);
 
             return switch (err) {
                 posix.EFAULT => error.BadAdress,
                 posix.EPERM => error.PermissionDenied,
                 else => unexpectedErrorPosix(err),
             };
-
         },
         Os.windows => {
             var utf16le_buf: [windows_util.PATH_MAX_WIDE]u16 = undefined;
std/os/path.zig
@@ -33,63 +33,103 @@ pub fn isSep(byte: u8) bool {
     }
 }
 
-/// Naively combines a series of paths with the native path seperator.
-/// Allocates memory for the result, which must be freed by the caller.
-
-pub fn join(allocator: *Allocator, paths: []const []const u8) ![]u8 {
-    assert(paths.len >= 1);
-    var total_paths_len: usize = paths.len; // 1 sep per path
-    {
-        var path_i: usize = 0;
-        while (path_i < paths.len) : (path_i += 1) {
-            const arg = ([]const u8)(paths[path_i]);
-            total_paths_len += arg.len;
+/// This is different from mem.join in that the separator will not be repeated if
+/// it is found at the end or beginning of a pair of consecutive paths.
+fn joinSep(allocator: *Allocator, separator: u8, paths: []const []const u8) ![]u8 {
+    if (paths.len == 0) return (([*]u8)(undefined))[0..0];
+
+    const total_len = blk: {
+        var sum: usize = paths[0].len;
+        var i: usize = 1;
+        while (i < paths.len) : (i += 1) {
+            const prev_path = paths[i - 1];
+            const this_path = paths[i];
+            const prev_sep = (prev_path.len != 0 and prev_path[prev_path.len - 1] == separator);
+            const this_sep = (this_path.len != 0 and this_path[0] == separator);
+            sum += @boolToInt(!prev_sep and !this_sep);
+            sum += if (prev_sep and this_sep) this_path.len - 1 else this_path.len;
         }
-    }
+        break :blk sum;
+    };
 
-    const buf = try allocator.alloc(u8, total_paths_len);
+    const buf = try allocator.alloc(u8, total_len);
     errdefer allocator.free(buf);
 
-    var buf_index: usize = 0;
-    var path_i: usize = 0;
-    while (true) {
-        const arg = ([]const u8)(paths[path_i]);
-        path_i += 1;
-        mem.copy(u8, buf[buf_index..], arg);
-        buf_index += arg.len;
-        if (path_i >= paths.len) break;
-        if (buf_index > 0 and buf[buf_index - 1] != sep) {
-            buf[buf_index] = sep;
+    mem.copy(u8, buf, paths[0]);
+    var buf_index: usize = paths[0].len;
+    var i: usize = 1;
+    while (i < paths.len) : (i += 1) {
+        const prev_path = paths[i - 1];
+        const this_path = paths[i];
+        const prev_sep = (prev_path.len != 0 and prev_path[prev_path.len - 1] == separator);
+        const this_sep = (this_path.len != 0 and this_path[0] == separator);
+        if (!prev_sep and !this_sep) {
+            buf[buf_index] = separator;
             buf_index += 1;
         }
+        const adjusted_path = if (prev_sep and this_sep) this_path[1..] else this_path;
+        mem.copy(u8, buf[buf_index..], adjusted_path);
+        buf_index += adjusted_path.len;
     }
 
-    return allocator.shrink(u8, buf, buf_index);
+    // No need for shrink since buf is exactly the correct size.
+    return buf;
+}
+
+pub const join = if (is_windows) joinWindows else joinPosix;
+
+/// Naively combines a series of paths with the native path seperator.
+/// Allocates memory for the result, which must be freed by the caller.
+pub fn joinWindows(allocator: *Allocator, paths: []const []const u8) ![]u8 {
+    return joinSep(allocator, sep_windows, paths);
+}
+
+/// Naively combines a series of paths with the native path seperator.
+/// Allocates memory for the result, which must be freed by the caller.
+pub fn joinPosix(allocator: *Allocator, paths: []const []const u8) ![]u8 {
+    return joinSep(allocator, sep_posix, paths);
+}
+
+fn testJoinWindows(paths: []const []const u8, expected: []const u8) void {
+    var buf: [1024]u8 = undefined;
+    const a = &std.heap.FixedBufferAllocator.init(&buf).allocator;
+    const actual = joinWindows(a, paths) catch @panic("fail");
+    debug.assertOrPanic(mem.eql(u8, actual, expected));
+}
+
+fn testJoinPosix(paths: []const []const u8, expected: []const u8) void {
+    var buf: [1024]u8 = undefined;
+    const a = &std.heap.FixedBufferAllocator.init(&buf).allocator;
+    const actual = joinPosix(a, paths) catch @panic("fail");
+    debug.assertOrPanic(mem.eql(u8, actual, expected));
 }
 
 test "os.path.join" {
-    switch (builtin.os) {
-        Os.windows => {
-            assert(mem.eql(u8, try join(debug.global_allocator, [][]const u8{"c:\\a\\b", "c"}), "c:\\a\\b\\c"));
-            assert(mem.eql(u8, try join(debug.global_allocator, [][]const u8{"c:\\a\\b\\", "c"}), "c:\\a\\b\\c"));
-            assert(mem.eql(u8, try join(debug.global_allocator, [][]const u8{"c:\\", "a", "b\\", "c"}), "c:\\a\\b\\c"));
-            assert(mem.eql(u8, try join(debug.global_allocator, [][]const u8{"c:\\a\\", "b\\", "c"}), "c:\\a\\b\\c"));
-            assert(mem.eql(u8, try join( debug.global_allocator
-                                       , [][]const u8{ "c:\\home\\andy\\dev\\zig\\build\\lib\\zig\\std"
-                                                     , "io.zig"})
-                                       , "c:\\home\\andy\\dev\\zig\\build\\lib\\zig\\std\\io.zig"));
-        },
-        else => {
-            assert(mem.eql(u8, try join(debug.global_allocator, [][]const u8{"/a/b", "c"}), "/a/b/c"));
-            assert(mem.eql(u8, try join(debug.global_allocator, [][]const u8{"/a/b/", "c"}), "/a/b/c"));
-            assert(mem.eql(u8, try join(debug.global_allocator, [][]const u8{"/", "a", "b/", "c"}), "/a/b/c"));
-            assert(mem.eql(u8, try join(debug.global_allocator, [][]const u8{"/a/", "b/", "c"}), "/a/b/c"));
-            assert(mem.eql(u8, try join( debug.global_allocator
-                                       , [][]const u8{ "/home/andy/dev/zig/build/lib/zig/std"
-                                                     , "io.zig"})
-                                       , "/home/andy/dev/zig/build/lib/zig/std/io.zig"));
-        }
-    }
+    testJoinWindows([][]const u8{ "c:\\a\\b", "c" }, "c:\\a\\b\\c");
+    testJoinWindows([][]const u8{ "c:\\a\\b", "c" }, "c:\\a\\b\\c");
+    testJoinWindows([][]const u8{ "c:\\a\\b\\", "c" }, "c:\\a\\b\\c");
+
+    testJoinWindows([][]const u8{ "c:\\", "a", "b\\", "c" }, "c:\\a\\b\\c");
+    testJoinWindows([][]const u8{ "c:\\a\\", "b\\", "c" }, "c:\\a\\b\\c");
+
+    testJoinWindows(
+        [][]const u8{ "c:\\home\\andy\\dev\\zig\\build\\lib\\zig\\std", "io.zig" },
+        "c:\\home\\andy\\dev\\zig\\build\\lib\\zig\\std\\io.zig",
+    );
+
+    testJoinPosix([][]const u8{ "/a/b", "c" }, "/a/b/c");
+    testJoinPosix([][]const u8{ "/a/b/", "c" }, "/a/b/c");
+
+    testJoinPosix([][]const u8{ "/", "a", "b/", "c" }, "/a/b/c");
+    testJoinPosix([][]const u8{ "/a/", "b/", "c" }, "/a/b/c");
+
+    testJoinPosix(
+        [][]const u8{ "/home/andy/dev/zig/build/lib/zig/std", "io.zig" },
+        "/home/andy/dev/zig/build/lib/zig/std/io.zig",
+    );
+
+    testJoinPosix([][]const u8{ "a", "/c" }, "a/c");
+    testJoinPosix([][]const u8{ "a/", "/c" }, "a/c");
 }
 
 pub fn isAbsolute(path: []const u8) bool {
@@ -335,18 +375,8 @@ fn asciiEqlIgnoreCase(s1: []const u8, s2: []const u8) bool {
     return true;
 }
 
-/// Converts the command line arguments into a slice and calls `resolveSlice`.
-pub fn resolve(allocator: *Allocator, args: ...) ![]u8 {
-    var paths: [args.len][]const u8 = undefined;
-    comptime var arg_i = 0;
-    inline while (arg_i < args.len) : (arg_i += 1) {
-        paths[arg_i] = args[arg_i];
-    }
-    return resolveSlice(allocator, paths);
-}
-
 /// On Windows, this calls `resolveWindows` and on POSIX it calls `resolvePosix`.
-pub fn resolveSlice(allocator: *Allocator, paths: []const []const u8) ![]u8 {
+pub fn resolve(allocator: *Allocator, paths: []const []const u8) ![]u8 {
     if (is_windows) {
         return resolveWindows(allocator, paths);
     } else {
@@ -625,7 +655,10 @@ test "os.path.resolveWindows" {
         const parsed_cwd = windowsParsePath(cwd);
         {
             const result = testResolveWindows([][]const u8{ "/usr/local", "lib\\zig\\std\\array_list.zig" });
-            const expected = try join(debug.global_allocator, [][]const u8{ parsed_cwd.disk_designator, "usr\\local\\lib\\zig\\std\\array_list.zig"});
+            const expected = try join(debug.global_allocator, [][]const u8{
+                parsed_cwd.disk_designator,
+                "usr\\local\\lib\\zig\\std\\array_list.zig",
+            });
             if (parsed_cwd.kind == WindowsPath.Kind.Drive) {
                 expected[0] = asciiUpper(parsed_cwd.disk_designator[0]);
             }
@@ -633,7 +666,10 @@ test "os.path.resolveWindows" {
         }
         {
             const result = testResolveWindows([][]const u8{ "usr/local", "lib\\zig" });
-            const expected = try join(debug.global_allocator, [][]const u8{ cwd, "usr\\local\\lib\\zig" });
+            const expected = try join(debug.global_allocator, [][]const u8{
+                cwd,
+                "usr\\local\\lib\\zig",
+            });
             if (parsed_cwd.kind == WindowsPath.Kind.Drive) {
                 expected[0] = asciiUpper(parsed_cwd.disk_designator[0]);
             }
std/build.zig
@@ -145,8 +145,8 @@ pub const Builder = struct {
 
     pub fn setInstallPrefix(self: *Builder, maybe_prefix: ?[]const u8) void {
         self.prefix = maybe_prefix orelse "/usr/local"; // TODO better default
-        self.lib_dir = os.path.join(self.allocator, [][]const u8{self.prefix, "lib"}) catch unreachable;
-        self.exe_dir = os.path.join(self.allocator, [][]const u8{self.prefix, "bin"}) catch unreachable;
+        self.lib_dir = os.path.join(self.allocator, [][]const u8{ self.prefix, "lib" }) catch unreachable;
+        self.exe_dir = os.path.join(self.allocator, [][]const u8{ self.prefix, "bin" }) catch unreachable;
     }
 
     pub fn addExecutable(self: *Builder, name: []const u8, root_src: ?[]const u8) *LibExeObjStep {
@@ -618,7 +618,10 @@ pub const Builder = struct {
 
     ///::dest_rel_path is relative to prefix path or it can be an absolute path
     pub fn addInstallFile(self: *Builder, src_path: []const u8, dest_rel_path: []const u8) *InstallFileStep {
-        const full_dest_path = os.path.resolve(self.allocator, self.prefix, dest_rel_path) catch unreachable;
+        const full_dest_path = os.path.resolve(
+            self.allocator,
+            [][]const u8{ self.prefix, dest_rel_path },
+        ) catch unreachable;
         self.pushInstalledFile(full_dest_path);
 
         const install_step = self.allocator.create(InstallFileStep) catch unreachable;
@@ -653,7 +656,7 @@ pub const Builder = struct {
     }
 
     fn pathFromRoot(self: *Builder, rel_path: []const u8) []u8 {
-        return os.path.resolve(self.allocator, self.build_root, rel_path) catch unreachable;
+        return os.path.resolve(self.allocator, [][]const u8{ self.build_root, rel_path }) catch unreachable;
     }
 
     pub fn fmt(self: *Builder, comptime format: []const u8, args: ...) []u8 {
@@ -676,7 +679,7 @@ pub const Builder = struct {
                 if (os.path.isAbsolute(name)) {
                     return name;
                 }
-                const full_path = try os.path.join(self.allocator, [][]const u8{search_prefix, "bin", self.fmt("{}{}", name, exe_extension)});
+                const full_path = try os.path.join(self.allocator, [][]const u8{ search_prefix, "bin", self.fmt("{}{}", name, exe_extension) });
                 if (os.path.real(self.allocator, full_path)) |real_path| {
                     return real_path;
                 } else |_| {
@@ -691,7 +694,7 @@ pub const Builder = struct {
                 }
                 var it = mem.tokenize(PATH, []u8{os.path.delimiter});
                 while (it.next()) |path| {
-                    const full_path = try os.path.join(self.allocator, [][]const u8{path, self.fmt("{}{}", name, exe_extension)});
+                    const full_path = try os.path.join(self.allocator, [][]const u8{ path, self.fmt("{}{}", name, exe_extension) });
                     if (os.path.real(self.allocator, full_path)) |real_path| {
                         return real_path;
                     } else |_| {
@@ -705,7 +708,7 @@ pub const Builder = struct {
                 return name;
             }
             for (paths) |path| {
-                const full_path = try os.path.join(self.allocator, [][]const u8{path, self.fmt("{}{}", name, exe_extension)});
+                const full_path = try os.path.join(self.allocator, [][]const u8{ path, self.fmt("{}{}", name, exe_extension) });
                 if (os.path.real(self.allocator, full_path)) |real_path| {
                     return real_path;
                 } else |_| {
@@ -1113,7 +1116,10 @@ pub const LibExeObjStep = struct {
     }
 
     pub fn getOutputPath(self: *LibExeObjStep) []const u8 {
-        return if (self.output_path) |output_path| output_path else os.path.join(self.builder.allocator, [][]const u8{self.builder.cache_root, self.out_filename}) catch unreachable;
+        return if (self.output_path) |output_path| output_path else os.path.join(
+            self.builder.allocator,
+            [][]const u8{ self.builder.cache_root, self.out_filename },
+        ) catch unreachable;
     }
 
     pub fn setOutputHPath(self: *LibExeObjStep, file_path: []const u8) void {
@@ -1126,7 +1132,10 @@ pub const LibExeObjStep = struct {
     }
 
     pub fn getOutputHPath(self: *LibExeObjStep) []const u8 {
-        return if (self.output_h_path) |output_h_path| output_h_path else os.path.join(self.builder.allocator, [][]const u8{self.builder.cache_root, self.out_h_filename}) catch unreachable;
+        return if (self.output_h_path) |output_h_path| output_h_path else os.path.join(
+            self.builder.allocator,
+            [][]const u8{ self.builder.cache_root, self.out_h_filename },
+        ) catch unreachable;
     }
 
     pub fn addAssemblyFile(self: *LibExeObjStep, path: []const u8) void {
@@ -1226,7 +1235,10 @@ pub const LibExeObjStep = struct {
         }
 
         if (self.build_options_contents.len() > 0) {
-            const build_options_file = try os.path.join(builder.allocator, [][]const u8{builder.cache_root, builder.fmt("{}_build_options.zig", self.name)});
+            const build_options_file = try os.path.join(
+                builder.allocator,
+                [][]const u8{ builder.cache_root, builder.fmt("{}_build_options.zig", self.name) },
+            );
             try std.io.writeFile(build_options_file, self.build_options_contents.toSliceConst());
             try zig_args.append("--pkg-begin");
             try zig_args.append("build_options");
@@ -1476,7 +1488,10 @@ pub const LibExeObjStep = struct {
                     cc_args.append("-c") catch unreachable;
                     cc_args.append(abs_source_file) catch unreachable;
 
-                    const cache_o_src = os.path.join(builder.allocator, [][]const u8{builder.cache_root, source_file}) catch unreachable;
+                    const cache_o_src = os.path.join(
+                        builder.allocator,
+                        [][]const u8{ builder.cache_root, source_file },
+                    ) catch unreachable;
                     if (os.path.dirname(cache_o_src)) |cache_o_dir| {
                         try builder.makePath(cache_o_dir);
                     }
@@ -1528,7 +1543,10 @@ pub const LibExeObjStep = struct {
                         cc_args.append("-current_version") catch unreachable;
                         cc_args.append(builder.fmt("{}.{}.{}", self.version.major, self.version.minor, self.version.patch)) catch unreachable;
 
-                        const install_name = builder.pathFromRoot(os.path.join(builder.allocator, [][]const u8{builder.cache_root, self.major_only_filename}) catch unreachable);
+                        const install_name = builder.pathFromRoot(os.path.join(
+                            builder.allocator,
+                            [][]const u8{ builder.cache_root, self.major_only_filename },
+                        ) catch unreachable);
                         cc_args.append("-install_name") catch unreachable;
                         cc_args.append(install_name) catch unreachable;
                     } else {
@@ -1594,7 +1612,10 @@ pub const LibExeObjStep = struct {
                     cc_args.append("-c") catch unreachable;
                     cc_args.append(abs_source_file) catch unreachable;
 
-                    const cache_o_src = os.path.join(builder.allocator, [][]const u8{builder.cache_root, source_file}) catch unreachable;
+                    const cache_o_src = os.path.join(
+                        builder.allocator,
+                        [][]const u8{ builder.cache_root, source_file },
+                    ) catch unreachable;
                     if (os.path.dirname(cache_o_src)) |cache_o_dir| {
                         try builder.makePath(cache_o_dir);
                     }
@@ -1757,7 +1778,10 @@ pub const TestStep = struct {
             return output_path;
         } else {
             const basename = self.builder.fmt("test{}", self.target.exeFileExt());
-            return os.path.join(self.builder.allocator, [][]const u8{self.builder.cache_root, basename}) catch unreachable;
+            return os.path.join(
+                self.builder.allocator,
+                [][]const u8{ self.builder.cache_root, basename },
+            ) catch unreachable;
         }
     }
 
@@ -1979,13 +2003,22 @@ const InstallArtifactStep = struct {
             .builder = builder,
             .step = Step.init(builder.fmt("install {}", artifact.step.name), builder.allocator, make),
             .artifact = artifact,
-            .dest_file = os.path.join(builder.allocator, dest_dir, artifact.out_filename) catch unreachable,
-        }) catch unreachable;
+            .dest_file = os.path.join(
+                builder.allocator,
+                [][]const u8{ dest_dir, artifact.out_filename },
+            ) catch unreachable,
+        };
         self.step.dependOn(&artifact.step);
         builder.pushInstalledFile(self.dest_file);
         if (self.artifact.kind == LibExeObjStep.Kind.Lib and !self.artifact.static) {
-            builder.pushInstalledFile(os.path.join(builder.allocator, [][]const u8{builder.lib_dir, artifact.major_only_filename}) catch unreachable);
-            builder.pushInstalledFile(os.path.join(builder.allocator, [][]const u8{builder.lib_dir, artifact.name_only_filename}) catch unreachable);
+            builder.pushInstalledFile(os.path.join(
+                builder.allocator,
+                [][]const u8{ builder.lib_dir, artifact.major_only_filename },
+            ) catch unreachable);
+            builder.pushInstalledFile(os.path.join(
+                builder.allocator,
+                [][]const u8{ builder.lib_dir, artifact.name_only_filename },
+            ) catch unreachable);
         }
         return self;
     }
@@ -2141,13 +2174,19 @@ fn doAtomicSymLinks(allocator: *Allocator, output_path: []const u8, filename_maj
     const out_dir = os.path.dirname(output_path) orelse ".";
     const out_basename = os.path.basename(output_path);
     // sym link for libfoo.so.1 to libfoo.so.1.2.3
-    const major_only_path = os.path.join(allocator, [][]const u8{out_dir, filename_major_only}) catch unreachable;
+    const major_only_path = os.path.join(
+        allocator,
+        [][]const u8{ out_dir, filename_major_only },
+    ) catch unreachable;
     os.atomicSymLink(allocator, out_basename, major_only_path) catch |err| {
         warn("Unable to symlink {} -> {}\n", major_only_path, out_basename);
         return err;
     };
     // sym link for libfoo.so to libfoo.so.1
-    const name_only_path = os.path.join(allocator, [][]const u8{out_dir, filename_name_only}) catch unreachable;
+    const name_only_path = os.path.join(
+        allocator,
+        [][]const u8{ out_dir, filename_name_only },
+    ) catch unreachable;
     os.atomicSymLink(allocator, filename_major_only, name_only_path) catch |err| {
         warn("Unable to symlink {} -> {}\n", name_only_path, filename_major_only);
         return err;
std/mem.zig
@@ -882,42 +882,37 @@ pub const SplitIterator = struct {
     }
 };
 
-/// Naively combines a series of strings with a separator.
+/// Naively combines a series of slices with a separator.
 /// Allocates memory for the result, which must be freed by the caller.
-pub fn join(allocator: *Allocator, sep: u8, strings: ...) ![]u8 {
-    comptime assert(strings.len >= 1);
-    var total_strings_len: usize = strings.len; // 1 sep per string
-    {
-        comptime var string_i = 0;
-        inline while (string_i < strings.len) : (string_i += 1) {
-            const arg = ([]const u8)(strings[string_i]);
-            total_strings_len += arg.len;
-        }
-    }
+pub fn join(allocator: *Allocator, separator: []const u8, slices: []const []const u8) ![]u8 {
+    if (slices.len == 0) return (([*]u8)(undefined))[0..0];
+
+    const total_len = blk: {
+        var sum: usize = separator.len * (slices.len - 1);
+        for (slices) |slice|
+            sum += slice.len;
+        break :blk sum;
+    };
 
-    const buf = try allocator.alloc(u8, total_strings_len);
+    const buf = try allocator.alloc(u8, total_len);
     errdefer allocator.free(buf);
 
-    var buf_index: usize = 0;
-    comptime var string_i = 0;
-    inline while (true) {
-        const arg = ([]const u8)(strings[string_i]);
-        string_i += 1;
-        copy(u8, buf[buf_index..], arg);
-        buf_index += arg.len;
-        if (string_i >= strings.len) break;
-        if (buf[buf_index - 1] != sep) {
-            buf[buf_index] = sep;
-            buf_index += 1;
-        }
+    copy(u8, buf, slices[0]);
+    var buf_index: usize = slices[0].len;
+    for (slices[1..]) |slice| {
+        copy(u8, buf[buf_index..], separator);
+        buf_index += separator.len;
+        copy(u8, buf[buf_index..], slice);
+        buf_index += slice.len;
     }
 
-    return allocator.shrink(u8, buf, buf_index);
+    // No need for shrink since buf is exactly the correct size.
+    return buf;
 }
 
 test "mem.join" {
-    assert(eql(u8, try join(debug.global_allocator, ',', "a", "b", "c"), "a,b,c"));
-    assert(eql(u8, try join(debug.global_allocator, ',', "a"), "a"));
+    assert(eql(u8, try join(debug.global_allocator, ",", [][]const u8{ "a", "b", "c" }), "a,b,c"));
+    assert(eql(u8, try join(debug.global_allocator, ",", [][]const u8{"a"}), "a"));
 }
 
 test "testStringEquality" {
test/cli.zig
@@ -27,7 +27,7 @@ pub fn main() !void {
         std.debug.warn("Expected second argument to be cache root directory path\n");
         return error.InvalidArgs;
     });
-    const zig_exe = try os.path.resolve(a, zig_exe_rel);
+    const zig_exe = try os.path.resolve(a, [][]const u8{zig_exe_rel});
 
     const dir_path = try os.path.join(a, [][]const u8{ cache_root, "clitest" });
     const TestFn = fn ([]const u8, []const u8) anyerror!void;
test/tests.zig
@@ -439,7 +439,10 @@ pub const CompareOutputContext = struct {
     pub fn addCase(self: *CompareOutputContext, case: TestCase) void {
         const b = self.b;
 
-        const root_src = os.path.join(b.allocator, [][]const u8{b.cache_root, case.sources.items[0].filename}) catch unreachable;
+        const root_src = os.path.join(
+            b.allocator,
+            [][]const u8{ b.cache_root, case.sources.items[0].filename },
+        ) catch unreachable;
 
         switch (case.special) {
             Special.Asm => {
@@ -452,7 +455,10 @@ pub const CompareOutputContext = struct {
                 exe.addAssemblyFile(root_src);
 
                 for (case.sources.toSliceConst()) |src_file| {
-                    const expanded_src_path = os.path.join(b.allocator, [][]const u8{b.cache_root, src_file.filename}) catch unreachable;
+                    const expanded_src_path = os.path.join(
+                        b.allocator,
+                        [][]const u8{ b.cache_root, src_file.filename },
+                    ) catch unreachable;
                     const write_src = b.addWriteFile(expanded_src_path, src_file.source);
                     exe.step.dependOn(&write_src.step);
                 }
@@ -476,7 +482,10 @@ pub const CompareOutputContext = struct {
                     }
 
                     for (case.sources.toSliceConst()) |src_file| {
-                        const expanded_src_path = os.path.join(b.allocator, [][]const u8{b.cache_root, src_file.filename}) catch unreachable;
+                        const expanded_src_path = os.path.join(
+                            b.allocator,
+                            [][]const u8{ b.cache_root, src_file.filename },
+                        ) catch unreachable;
                         const write_src = b.addWriteFile(expanded_src_path, src_file.source);
                         exe.step.dependOn(&write_src.step);
                     }
@@ -499,7 +508,10 @@ pub const CompareOutputContext = struct {
                 }
 
                 for (case.sources.toSliceConst()) |src_file| {
-                    const expanded_src_path = os.path.join(b.allocator, [][]const u8{b.cache_root, src_file.filename}) catch unreachable;
+                    const expanded_src_path = os.path.join(
+                        b.allocator,
+                        [][]const u8{ b.cache_root, src_file.filename },
+                    ) catch unreachable;
                     const write_src = b.addWriteFile(expanded_src_path, src_file.source);
                     exe.step.dependOn(&write_src.step);
                 }
@@ -572,8 +584,14 @@ pub const CompileErrorContext = struct {
             const self = @fieldParentPtr(CompileCmpOutputStep, "step", step);
             const b = self.context.b;
 
-            const root_src = os.path.join(b.allocator, [][]const u8{b.cache_root, self.case.sources.items[0].filename}) catch unreachable;
-            const obj_path = os.path.join(b.allocator, [][]const u8{b.cache_root, "test.o"}) catch unreachable;
+            const root_src = os.path.join(
+                b.allocator,
+                [][]const u8{ b.cache_root, self.case.sources.items[0].filename },
+            ) catch unreachable;
+            const obj_path = os.path.join(
+                b.allocator,
+                [][]const u8{ b.cache_root, "test.o" },
+            ) catch unreachable;
 
             var zig_args = ArrayList([]const u8).init(b.allocator);
             zig_args.append(b.zig_exe) catch unreachable;
@@ -721,7 +739,10 @@ pub const CompileErrorContext = struct {
             self.step.dependOn(&compile_and_cmp_errors.step);
 
             for (case.sources.toSliceConst()) |src_file| {
-                const expanded_src_path = os.path.join(b.allocator, [][]const u8{b.cache_root, src_file.filename}) catch unreachable;
+                const expanded_src_path = os.path.join(
+                    b.allocator,
+                    [][]const u8{ b.cache_root, src_file.filename },
+                ) catch unreachable;
                 const write_src = b.addWriteFile(expanded_src_path, src_file.source);
                 compile_and_cmp_errors.step.dependOn(&write_src.step);
             }
@@ -852,7 +873,10 @@ pub const TranslateCContext = struct {
             const self = @fieldParentPtr(TranslateCCmpOutputStep, "step", step);
             const b = self.context.b;
 
-            const root_src = os.path.join(b.allocator, [][]const u8{b.cache_root, self.case.sources.items[0].filename}) catch unreachable;
+            const root_src = os.path.join(
+                b.allocator,
+                [][]const u8{ b.cache_root, self.case.sources.items[0].filename },
+            ) catch unreachable;
 
             var zig_args = ArrayList([]const u8).init(b.allocator);
             zig_args.append(b.zig_exe) catch unreachable;
@@ -986,7 +1010,10 @@ pub const TranslateCContext = struct {
         self.step.dependOn(&translate_c_and_cmp.step);
 
         for (case.sources.toSliceConst()) |src_file| {
-            const expanded_src_path = os.path.join(b.allocator, [][]const u8{b.cache_root, src_file.filename}) catch unreachable;
+            const expanded_src_path = os.path.join(
+                b.allocator,
+                [][]const u8{ b.cache_root, src_file.filename },
+            ) catch unreachable;
             const write_src = b.addWriteFile(expanded_src_path, src_file.source);
             translate_c_and_cmp.step.dependOn(&write_src.step);
         }
@@ -1101,7 +1128,10 @@ pub const GenHContext = struct {
 
     pub fn addCase(self: *GenHContext, case: *const TestCase) void {
         const b = self.b;
-        const root_src = os.path.join(b.allocator, [][]const u8{b.cache_root, case.sources.items[0].filename}) catch unreachable;
+        const root_src = os.path.join(
+            b.allocator,
+            [][]const u8{ b.cache_root, case.sources.items[0].filename },
+        ) catch unreachable;
 
         const mode = builtin.Mode.Debug;
         const annotated_case_name = fmt.allocPrint(self.b.allocator, "gen-h {} ({})", case.name, @tagName(mode)) catch unreachable;
@@ -1113,7 +1143,10 @@ pub const GenHContext = struct {
         obj.setBuildMode(mode);
 
         for (case.sources.toSliceConst()) |src_file| {
-            const expanded_src_path = os.path.join(b.allocator, [][]const u8{b.cache_root, src_file.filename}) catch unreachable;
+            const expanded_src_path = os.path.join(
+                b.allocator,
+                [][]const u8{ b.cache_root, src_file.filename },
+            ) catch unreachable;
             const write_src = b.addWriteFile(expanded_src_path, src_file.source);
             obj.step.dependOn(&write_src.step);
         }
build.zig
@@ -16,7 +16,10 @@ pub fn build(b: *Builder) !void {
     var docgen_exe = b.addExecutable("docgen", "doc/docgen.zig");
 
     const rel_zig_exe = try os.path.relative(b.allocator, b.build_root, b.zig_exe);
-    const langref_out_path = os.path.join(b.allocator, [][]const u8{ b.cache_root, "langref.html" }) catch unreachable;
+    const langref_out_path = os.path.join(
+        b.allocator,
+        [][]const u8{ b.cache_root, "langref.html" },
+    ) catch unreachable;
     var docgen_cmd = b.addCommand(null, b.env_map, [][]const u8{
         docgen_exe.getOutputPath(),
         rel_zig_exe,
@@ -125,13 +128,19 @@ fn dependOnLib(b: *Builder, lib_exe_obj: var, dep: LibraryDep) void {
     for (dep.libdirs.toSliceConst()) |lib_dir| {
         lib_exe_obj.addLibPath(lib_dir);
     }
-    const lib_dir = os.path.join(b.allocator, [][]const u8{dep.prefix, "lib"}) catch unreachable;
+    const lib_dir = os.path.join(
+        b.allocator,
+        [][]const u8{ dep.prefix, "lib" },
+    ) catch unreachable;
     for (dep.system_libs.toSliceConst()) |lib| {
         const static_bare_name = if (mem.eql(u8, lib, "curses"))
             ([]const u8)("libncurses.a")
         else
             b.fmt("lib{}.a", lib);
-        const static_lib_name = os.path.join(b.allocator, [][]const u8{lib_dir, static_bare_name}) catch unreachable;
+        const static_lib_name = os.path.join(
+            b.allocator,
+            [][]const u8{ lib_dir, static_bare_name },
+        ) catch unreachable;
         const have_static = fileExists(static_lib_name) catch unreachable;
         if (have_static) {
             lib_exe_obj.addObjectFile(static_lib_name);
@@ -159,7 +168,11 @@ fn fileExists(filename: []const u8) !bool {
 
 fn addCppLib(b: *Builder, lib_exe_obj: var, cmake_binary_dir: []const u8, lib_name: []const u8) void {
     const lib_prefix = if (lib_exe_obj.target.isWindows()) "" else "lib";
-    lib_exe_obj.addObjectFile(os.path.join(b.allocator, [][]const u8{ cmake_binary_dir, "zig_cpp", b.fmt("{}{}{}", lib_prefix, lib_name, lib_exe_obj.target.libFileExt()) }) catch unreachable);
+    lib_exe_obj.addObjectFile(os.path.join(b.allocator, [][]const u8{
+        cmake_binary_dir,
+        "zig_cpp",
+        b.fmt("{}{}{}", lib_prefix, lib_name, lib_exe_obj.target.libFileExt()),
+    }) catch unreachable);
 }
 
 const LibraryDep = struct {
@@ -235,8 +248,11 @@ fn findLLVM(b: *Builder, llvm_config_exe: []const u8) !LibraryDep {
 pub fn installStdLib(b: *Builder, stdlib_files: []const u8) void {
     var it = mem.tokenize(stdlib_files, ";");
     while (it.next()) |stdlib_file| {
-        const src_path = os.path.join(b.allocator, [][]const u8{"std", stdlib_file}) catch unreachable;
-        const dest_path = os.path.join(b.allocator, [][]const u8{"lib", "zig", "std", stdlib_file}) catch unreachable;
+        const src_path = os.path.join(b.allocator, [][]const u8{ "std", stdlib_file }) catch unreachable;
+        const dest_path = os.path.join(
+            b.allocator,
+            [][]const u8{ "lib", "zig", "std", stdlib_file },
+        ) catch unreachable;
         b.installFile(src_path, dest_path);
     }
 }
@@ -244,8 +260,11 @@ pub fn installStdLib(b: *Builder, stdlib_files: []const u8) void {
 pub fn installCHeaders(b: *Builder, c_header_files: []const u8) void {
     var it = mem.tokenize(c_header_files, ";");
     while (it.next()) |c_header_file| {
-        const src_path = os.path.join(b.allocator, [][]const u8{"c_headers", c_header_file}) catch unreachable;
-        const dest_path = os.path.join(b.allocator, [][]const u8{"lib", "zig", "include", c_header_file}) catch unreachable;
+        const src_path = os.path.join(b.allocator, [][]const u8{ "c_headers", c_header_file }) catch unreachable;
+        const dest_path = os.path.join(
+            b.allocator,
+            [][]const u8{ "lib", "zig", "include", c_header_file },
+        ) catch unreachable;
         b.installFile(src_path, dest_path);
     }
 }