From 9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 9 Aug 2026 10:41:33 -0300 Subject: replace ArrayLists with bounded storage --- tools/embed_zig_sources.zig | 75 ++++++++++++++++++++++++++---------- tools/gen_themes.zig | 93 +++++++++++++++++++++++++++++++++------------ 2 files changed, 123 insertions(+), 45 deletions(-) (limited to 'tools') diff --git a/tools/embed_zig_sources.zig b/tools/embed_zig_sources.zig index a832798c..61427415 100644 --- a/tools/embed_zig_sources.zig +++ b/tools/embed_zig_sources.zig @@ -13,15 +13,12 @@ pub fn main(init: std.process.Init) !void { const root = args[1]; const output_path = args[2]; - var paths: std.ArrayList([]const u8) = .empty; + const paths = (try gitTrackedZigFiles(gpa, io, root)) orelse try walkedZigFiles(gpa, io, root); defer { - for (paths.items) |path| gpa.free(path); - paths.deinit(gpa); + for (paths) |path| gpa.free(path); + gpa.free(paths); } - - if (!try gitTrackedZigFiles(gpa, io, root, &paths)) - try walkedZigFiles(gpa, io, root, &paths); - std.mem.sort([]const u8, paths.items, {}, lessThanPath); + std.mem.sort([]const u8, paths, {}, lessThanPath); var generated: std.Io.Writer.Allocating = .init(gpa); defer generated.deinit(); @@ -32,7 +29,7 @@ pub fn main(init: std.process.Init) !void { \\ ); - for (paths.items) |path| { + for (paths) |path| { const full_path = try std.Io.Dir.path.join(gpa, &.{ root, path }); defer gpa.free(full_path); const contents = try std.Io.Dir.cwd().readFileAlloc(io, full_path, gpa, .limited(64 * 1024 * 1024)); @@ -52,40 +49,75 @@ pub fn main(init: std.process.Init) !void { try output.writeStreamingAll(io, generated.written()); } -/// Returns true only when Git ran successfully. The working-tree contents are -/// read later; Git supplies the tracked path filter, not historical blobs. +/// Returns null only when Git cannot supply the tracked-file list. Working-tree +/// contents are read later; Git supplies the path filter, not historical blobs. fn gitTrackedZigFiles( gpa: std.mem.Allocator, io: std.Io, root: []const u8, - paths: *std.ArrayList([]const u8), -) !bool { +) !?[][]const u8 { const result = std.process.run(gpa, io, .{ .argv = &.{ "git", "-C", root, "ls-files", "-z", "--", "*.zig" }, .stdout_limit = .limited(16 * 1024 * 1024), .stderr_limit = .limited(1024 * 1024), - }) catch return false; + }) catch return null; defer gpa.free(result.stdout); defer gpa.free(result.stderr); switch (result.term) { - .exited => |code| if (code != 0) return false, - else => return false, + .exited => |code| if (code != 0) return null, + else => return null, + } + + var count: usize = 0; + var count_it = std.mem.splitScalar(u8, result.stdout, 0); + while (count_it.next()) |raw_path| { + if (raw_path.len == 0 or !std.mem.endsWith(u8, raw_path, ".zig")) continue; + count += 1; } + const paths = try gpa.alloc([]const u8, count); + errdefer gpa.free(paths); + var index: usize = 0; + errdefer for (paths[0..index]) |path| gpa.free(path); var it = std.mem.splitScalar(u8, result.stdout, 0); while (it.next()) |raw_path| { if (raw_path.len == 0 or !std.mem.endsWith(u8, raw_path, ".zig")) continue; - try paths.append(gpa, try normalizedPath(gpa, raw_path)); + paths[index] = try normalizedPath(gpa, raw_path); + index += 1; } - return true; + std.debug.assert(index == paths.len); + return paths; } fn walkedZigFiles( gpa: std.mem.Allocator, io: std.Io, root: []const u8, - paths: *std.ArrayList([]const u8), -) !void { +) ![][]const u8 { + var count: usize = 0; + { + var dir = if (std.Io.Dir.path.isAbsolute(root)) + try std.Io.Dir.openDirAbsolute(io, root, .{ .iterate = true }) + else + try std.Io.Dir.cwd().openDir(io, root, .{ .iterate = true }); + defer dir.close(io); + var walker = try dir.walk(gpa); + defer walker.deinit(); + + while (try walker.next(io)) |entry| { + if (entry.kind == .directory and ignoredFallbackDir(entry.path)) { + walker.leave(io); + continue; + } + if ((entry.kind != .file and entry.kind != .sym_link) or !std.mem.endsWith(u8, entry.path, ".zig")) continue; + count += 1; + } + } + + const paths = try gpa.alloc([]const u8, count); + errdefer gpa.free(paths); + var index: usize = 0; + errdefer for (paths[0..index]) |path| gpa.free(path); var dir = if (std.Io.Dir.path.isAbsolute(root)) try std.Io.Dir.openDirAbsolute(io, root, .{ .iterate = true }) else @@ -100,8 +132,11 @@ fn walkedZigFiles( continue; } if ((entry.kind != .file and entry.kind != .sym_link) or !std.mem.endsWith(u8, entry.path, ".zig")) continue; - try paths.append(gpa, try normalizedPath(gpa, entry.path)); + paths[index] = try normalizedPath(gpa, entry.path); + index += 1; } + std.debug.assert(index == paths.len); + return paths; } fn ignoredFallbackDir(path: []const u8) bool { diff --git a/tools/gen_themes.zig b/tools/gen_themes.zig index 23bcad75..3add2040 100644 --- a/tools/gen_themes.zig +++ b/tools/gen_themes.zig @@ -78,32 +78,54 @@ pub fn main(init: std.process.Init) !void { const args = try init.minimal.args.toSlice(arena); if (args.len < 3) std.process.fatal("usage: gen-themes ...", .{}); - var themes: std.ArrayList(Theme) = .empty; + const sources = args[2..]; + const texts = try arena.alloc([]const u8, sources.len); + var helix_count: usize = 0; + var theme_count: usize = 0; + for (sources, texts) |src, *text| { + text.* = try std.Io.Dir.cwd().readFileAlloc(io, src, arena, .limited(8 << 20)); + if (std.mem.endsWith(u8, src, ".toml")) { + helix_count += 1; + theme_count += 1; + } else if (std.mem.endsWith(u8, src, ".json")) { + theme_count += try zedThemeCount(gpa, src, text.*); + } else { + std.process.fatal("{s}: not a helix .toml or a zed .json", .{src}); + } + } + + const themes = try arena.alloc(Theme, theme_count); + const stems = try arena.alloc([]const u8, helix_count); // TWO passes over the helix half, because `inherits = "other"` names a // SIBLING SOURCE by stem: nothing can be mapped until every file is read, // and the parent may be later in argv (or inherit something itself). var tomls: std.StringHashMapUnmanaged(Toml) = .empty; - var stems: std.ArrayList([]const u8) = .empty; - for (args[2..]) |src| { - const text = try std.Io.Dir.cwd().readFileAlloc(io, src, arena, .limited(8 << 20)); + var theme_index: usize = 0; + var stem_index: usize = 0; + for (sources, texts) |src, text| { if (std.mem.endsWith(u8, src, ".toml")) { const stem = std.fs.path.stem(src); try tomls.put(arena, stem, parseToml(arena, src, text)); - try stems.append(arena, stem); - } else if (std.mem.endsWith(u8, src, ".json")) - try readZed(arena, gpa, src, text, &themes) - else - std.process.fatal("{s}: not a helix .toml or a zed .json", .{src}); + stems[stem_index] = stem; + stem_index += 1; + } else { + theme_index += try readZed(arena, gpa, src, text, themes[theme_index..]); + } + } + std.debug.assert(stem_index == stems.len); + for (stems) |stem| { + themes[theme_index] = try helixTheme(arena, stem, resolve(arena, &tomls, stem, 8)); + theme_index += 1; } - for (stems.items) |stem| try helixTheme(arena, stem, resolve(arena, &tomls, stem, 8), &themes); + std.debug.assert(theme_index == themes.len); // sorted, so the ring's tail is the same on every machine whatever order // the directory happened to be read in - std.mem.sort(Theme, themes.items, {}, byName); + std.mem.sort(Theme, themes, {}, byName); var dir = try std.Io.Dir.cwd().openDir(io, args[1], .{}); defer dir.close(io); var buf: std.Io.Writer.Allocating = .init(arena); - for (themes.items) |t| { + for (themes) |t| { buf.clearRetainingCapacity(); try emit(&buf.writer, t); try write(io, dir, arena, t.name, buf.written()); @@ -113,7 +135,7 @@ pub fn main(init: std.process.Init) !void { // from whoever can. pardes.zig folds its declarations into the ring. buf.clearRetainingCapacity(); try buf.writer.writeAll("// Generated by tools/gen_themes.zig. Do not edit.\n"); - for (themes.items) |t| try buf.writer.print("pub const {s} = @import(\"{s}.zig\");\n", .{ t.name, t.name }); + for (themes) |t| try buf.writer.print("pub const {s} = @import(\"{s}.zig\");\n", .{ t.name, t.name }); try write(io, dir, arena, "list", buf.written()); } @@ -160,13 +182,20 @@ fn rgb(w: *std.Io.Writer, c: Rgb) !void { /// single word (a `Theme ` row is executed by splitting on the first /// space) and a legal identifier (it is a decl in list.zig). fn slug(arena: std.mem.Allocator, raw: []const u8) ![]const u8 { - var out: std.ArrayList(u8) = .empty; + const out = try arena.alloc(u8, raw.len); + var len: usize = 0; for (raw) |c| { - if (std.ascii.isAlphanumeric(c)) try out.append(arena, std.ascii.toLower(c)) - // one underscore per run of punctuation, and never a trailing one - else if (out.items.len > 0 and out.items[out.items.len - 1] != '_') try out.append(arena, '_'); + if (std.ascii.isAlphanumeric(c)) { + out[len] = std.ascii.toLower(c); + len += 1; + } else if (len > 0 and out[len - 1] != '_') { + // one underscore per run of punctuation; it is trimmed below when + // the run ends the name + out[len] = '_'; + len += 1; + } } - const s = std.mem.trim(u8, out.items, "_"); + const s = std.mem.trim(u8, out[0..len], "_"); if (s.len == 0 or !std.ascii.isAlphabetic(s[0])) std.process.fatal("theme name {s} does not slug to an identifier", .{raw}); return s; } @@ -284,7 +313,7 @@ fn resolve(arena: std.mem.Allocator, tomls: *const std.StringHashMapUnmanaged(To /// kw/str/comment their scope's fg, else ink (helix: unstyled = ui.text) /// num constant.numeric.fg, else constant.fg, else ink /// lineno ui.linenr.fg, else ink -fn helixTheme(arena: std.mem.Allocator, stem: []const u8, h: Helix, out: *std.ArrayList(Theme)) !void { +fn helixTheme(arena: std.mem.Allocator, stem: []const u8, h: Helix) !Theme { const fg = h.get("ui.text", .fg); const bg = h.get("ui.background", .bg); const ink = fg orelse ansi_default[7]; @@ -324,7 +353,7 @@ fn helixTheme(arena: std.mem.Allocator, stem: []const u8, h: Helix, out: *std.Ar // one line up, which is the reverse-video case again: those two would paint // the highlight in one colour on itself. const named_ink = h.get("ui.selection", .fg); - try out.append(arena, .{ + return .{ .name = try slug(arena, stem), // `palette` stays null: helix leaves the colors a PROGRAM emits alone, // and so do we. @@ -349,7 +378,7 @@ fn helixTheme(arena: std.mem.Allocator, stem: []const u8, h: Helix, out: *std.Ar .scroll_track = h.get("ui.statusline", .bg) orelse page, .scroll_thumb = sel, .palette = null, - }); + }; } /// helix lets a theme name one of the terminal's sixteen colors instead of an @@ -510,7 +539,19 @@ fn table(s: []const u8) Pair { // take them: a light theme is only usable if the shell's own output is // repainted for it, and pardes's acme-light is the hand-written proof. -fn readZed(arena: std.mem.Allocator, gpa: std.mem.Allocator, path: []const u8, text: []const u8, out: *std.ArrayList(Theme)) !void { +fn zedThemeCount(gpa: std.mem.Allocator, path: []const u8, text: []const u8) !usize { + const parsed = std.json.parseFromSlice(std.json.Value, gpa, text, .{}) catch + std.process.fatal("{s}: not valid JSON", .{path}); + defer parsed.deinit(); + const root = switch (parsed.value) { + .object => |o| o, + else => std.process.fatal("{s}: top level is not an object", .{path}), + }; + const list = root.get("themes") orelse std.process.fatal("{s}: no `themes` array", .{path}); + return list.array.items.len; +} + +fn readZed(arena: std.mem.Allocator, gpa: std.mem.Allocator, path: []const u8, text: []const u8, out: []Theme) !usize { const parsed = std.json.parseFromSlice(std.json.Value, gpa, text, .{}) catch std.process.fatal("{s}: not valid JSON", .{path}); defer parsed.deinit(); @@ -519,7 +560,8 @@ fn readZed(arena: std.mem.Allocator, gpa: std.mem.Allocator, path: []const u8, t else => std.process.fatal("{s}: top level is not an object", .{path}), }; const list = root.get("themes") orelse std.process.fatal("{s}: no `themes` array", .{path}); - for (list.array.items) |variant| { + std.debug.assert(list.array.items.len <= out.len); + for (list.array.items, 0..) |variant, index| { const v = variant.object; const z: Zed = .{ .path = path, @@ -531,7 +573,7 @@ fn readZed(arena: std.mem.Allocator, gpa: std.mem.Allocator, path: []const u8, t pal[i] = z.need(try std.fmt.allocPrint(arena, "terminal.ansi.{s}", .{n})); pal[i + 8] = z.need(try std.fmt.allocPrint(arena, "terminal.ansi.bright_{s}", .{n})); } - try out.append(arena, .{ + out[index] = .{ // `_zed` on every one of them: see the naming note at the top — // zed re-ships gruvbox, ayu and one, so the source is part of the // name rather than a suffix that appears only on the eight that @@ -563,8 +605,9 @@ fn readZed(arena: std.mem.Allocator, gpa: std.mem.Allocator, path: []const u8, t .scroll_track = z.need("scrollbar.thumb.border"), .scroll_thumb = z.need("scrollbar.thumb.background"), .palette = pal, - }); + }; } + return list.array.items.len; } /// zed's order, which is also the ANSI one: 0-7 plain, 8-15 the `bright_` half -- cgit v1.3