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 --- src/builtins.zig | 154 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 110 insertions(+), 44 deletions(-) (limited to 'src/builtins.zig') diff --git a/src/builtins.zig b/src/builtins.zig index ee542e77..3ccbdeed 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -54,36 +54,39 @@ pub const Ctx = struct { arg: ?[]const u8, }; -/// Every builtin, in the order they are written below — which is the enum's -/// numeric order. Nothing reads that order: the topbar picks its own subset by -/// name and Help sorts by key path, and nothing persists a builtin's integer -/// (the dump stores tag WORDS), so reordering this file is free. -/// -/// A FUNCTION and not a const, and so is Builtin below, for one reason: both -/// walk this file's own declaration list, and a const doing that is a -/// declaration whose value depends on itself — zig rejects it outright. As -/// functions they are only ever a signature to the walk, never a value, so -/// they are not in their own way. -/// -/// The walk only ever sees `pub` decls, so the imports above are invisible to -/// it; what it does see and turn away is `all`/`word`/`Builtin` (not types) -/// and `Ctx` (a type, but with no `run`). -pub fn all() []const type { - // the whole body is comptime: a []const type only exists there, and it is - // what makes the decl walk's `d` a compile-time name rather than a value +/// Every enabled builtin, in source order. Feature gates are explicit data; +/// a declaration that claims to be enabled but has the wrong run signature is +/// a compile error instead of silently disappearing from the command enum. +fn isEnabled(comptime T: type) bool { + return !@hasDecl(T, "enabled") or T.enabled; +} + +fn builtinCount() comptime_int { comptime { - @setEvalBranchQuota(4000); // one pass per decl, and @hasDecl builds a map each - var list: []const type = &.{}; + var count = 0; for (@typeInfo(@This()).@"struct".decls) |d| { - // @TypeOf never evaluates its operand, so this turns away `all` - // and `Builtin` by their SIGNATURES — asking for either one's - // VALUE here would be a declaration that depends on itself if (@TypeOf(@field(@This(), d.name)) != type) continue; const T = @field(@This(), d.name); - if (@typeInfo(T) != .@"struct") continue; - if (!@hasDecl(T, "run")) continue; - if (@TypeOf(T.run) != fn (Ctx) void) continue; - list = list ++ &[_]type{T}; + if (@typeInfo(T) != .@"struct" or !@hasDecl(T, "run") or !isEnabled(T)) continue; + if (@TypeOf(T.run) != fn (Ctx) void) + @compileError(d.name ++ ".run must have signature fn (Ctx) void"); + count += 1; + } + return count; + } +} + +fn builtinList() [builtinCount()]type { + comptime { + @setEvalBranchQuota(4000); + var list: [builtinCount()]type = undefined; + var count = 0; + for (@typeInfo(@This()).@"struct".decls) |d| { + if (@TypeOf(@field(@This(), d.name)) != type) continue; + const T = @field(@This(), d.name); + if (@typeInfo(T) != .@"struct" or !@hasDecl(T, "run") or !isEnabled(T)) continue; + list[count] = T; + count += 1; } return list; } @@ -100,16 +103,45 @@ pub fn word(comptime T: type) []const u8 { return n[dot + 1 ..]; } +pub const OutputTraits = struct { + name: []const u8, + steps: bool = false, + jumps: bool = false, + executes: bool = false, +}; + /// The enum: field name = struct name, value = index into `all()`. Everything /// downstream (leader_path's EnumArray, leader_rows, the topbar's comptime /// check, stringToEnum) speaks it exactly as it did when it was hand-written. -pub fn Builtin() type { - const cmds = all(); - const Tag = std.math.IntFittingRange(0, cmds.len - 1); - var names: [cmds.len][]const u8 = undefined; - for (cmds, 0..) |T, i| names[i] = word(T); - return @Enum(Tag, .exhaustive, &names, &std.simd.iota(Tag, cmds.len)); -} +/// +/// Registry-dependent APIs live in one namespace so the outer declaration +/// walk only sees this namespace's type, not functions whose signatures depend +/// on the builtin enum being constructed. +pub const registry = struct { + pub fn all() [builtinCount()]type { + return builtinList(); + } + + pub fn Builtin() type { + const cmds = builtinList(); + const Tag = std.math.IntFittingRange(0, cmds.len - 1); + var names: [cmds.len][]const u8 = undefined; + for (cmds, 0..) |T, i| names[i] = word(T); + return @Enum(Tag, .exhaustive, &names, &std.simd.iota(Tag, cmds.len)); + } + + pub fn takesArg(b: Builtin()) bool { + inline for (builtinList(), 0..) |T, i| + if (@intFromEnum(b) == i) return @hasDecl(T, "takes_arg") and T.takes_arg; + unreachable; + } + + pub fn outputTraits(b: Builtin()) ?OutputTraits { + inline for (builtinList(), 0..) |T, i| + if (@intFromEnum(b) == i) return if (@hasDecl(T, "output")) T.output else null; + unreachable; + } +}; // ---- the two acme verbs ---- @@ -128,12 +160,14 @@ pub fn Builtin() type { // way `Save` on a terminal is inert. pub const Look = struct { + pub const takes_arg = true; pub fn run(c: Ctx) void { c.p.lookAt(c.id, c.arg orelse return); } }; pub const Exec = struct { + pub const takes_arg = true; pub fn run(c: Ctx) void { // the destination pane is Look's business (it focuses what answered); // an execute deliberately leaves you where you were @@ -157,6 +191,7 @@ pub const Dump = struct { }; pub const Restore = struct { + pub const takes_arg = true; pub fn run(c: Ctx) void { var it = std.mem.tokenizeAny(u8, c.txt, " \t"); _ = it.next(); // the word "Restore" @@ -227,6 +262,7 @@ pub const NextColor = struct { /// nobody will ever measure. 228 short string compares is microseconds, and it /// happens once per theme change, not once per frame. pub const Theme = struct { + pub const takes_arg = true; pub fn run(c: Ctx) void { const want = std.mem.trim(u8, c.arg orelse return, " \t\r\n"); for (pardes.themes, 0..) |t, i| { @@ -248,6 +284,7 @@ pub const Theme = struct { /// reason Theme has neither. Panes already open keep the shell they are /// running; this is what the next one starts. pub const Shell = struct { + pub const takes_arg = true; pub fn run(c: Ctx) void { const want = std.mem.trim(u8, c.arg orelse return, " \t\r\n"); // A name too long for the buffer is not silently truncated into a @@ -265,8 +302,9 @@ pub const Shell = struct { /// each theme in turn — the picker is the list, and there is nothing to /// confirm because arriving already applied it. pub const ThemeSel = struct { + pub const output: OutputTraits = .{ .name = config.themes_buffer, .steps = true, .executes = true }; pub fn run(c: Ctx) void { - output_pane.openThemes(c.p, c.id); + output_pane.openThemes(c.p, c.id) catch |err| c.p.reportError(c.id, "themes", err); } }; @@ -304,7 +342,11 @@ pub const Crt = struct { /// — so this asks: the resolved PATH goes in fonts.want, the shell takes it on /// its next pass and re-rasters. Exactly the shape Restore already has. pub const Font = struct { - pub const run = if (pardes.platform == .gui) apply else {}; + pub const takes_arg = true; + pub const enabled = pardes.platform == .gui; + pub fn run(c: Ctx) void { + if (comptime enabled) apply(c) else unreachable; + } fn apply(c: Ctx) void { const want = std.mem.trim(u8, c.arg orelse return, " \t\r\n"); const hit = fonts.list(c.p.scratch.allocator(), want); @@ -326,9 +368,13 @@ pub const Font = struct { /// unreadable one — and this picker EXECUTES what it steps onto, so listing /// them would mean the list wearing one on the way past. See fonts.monospaced. pub const FontSel = struct { - pub const run = if (pardes.platform == .gui) apply else {}; + pub const output: OutputTraits = .{ .name = config.fonts_buffer, .steps = true, .executes = true }; + pub const enabled = pardes.platform == .gui; + pub fn run(c: Ctx) void { + if (comptime enabled) apply(c) else unreachable; + } fn apply(c: Ctx) void { - output_pane.openFonts(c.p, c.id); + output_pane.openFonts(c.p, c.id) catch |err| c.p.reportError(c.id, "fonts", err); } }; @@ -338,7 +384,10 @@ pub const FontSel = struct { /// omits it entirely, so the enum, Help and runtime binary contain no /// PdfFit. pub const PdfFit = struct { - pub const run = if (pardes.pdf_enabled) apply else {}; + pub const enabled = pardes.pdf_enabled; + pub fn run(c: Ctx) void { + if (comptime enabled) apply(c) else unreachable; + } fn apply(c: Ctx) void { c.p.togglePdfFit(c.pane); } @@ -348,7 +397,10 @@ pub const PdfFit = struct { /// filter, and a full theme duotone. It has the same compile-time and /// pane-local shape as PdfFit: absent without MuPDF and inert off a PDF pane. pub const PdfTint = struct { - pub const run = if (pardes.pdf_enabled) apply else {}; + pub const enabled = pardes.pdf_enabled; + pub fn run(c: Ctx) void { + if (comptime enabled) apply(c) else unreachable; + } fn apply(c: Ctx) void { c.p.togglePdfTint(c.pane); } @@ -358,7 +410,11 @@ pub const PdfTint = struct { /// command is absent from non-MuPDF builds and deliberately inert on every /// other pane kind, like the two PDF display toggles above. pub const PdfSections = struct { - pub const run = if (pardes.pdf_enabled) apply else {}; + pub const output: OutputTraits = .{ .name = config.pdf_sections_buffer, .steps = true }; + pub const enabled = pardes.pdf_enabled; + pub fn run(c: Ctx) void { + if (comptime enabled) apply(c) else unreachable; + } fn apply(c: Ctx) void { c.p.openPdfSections(c.id); } @@ -474,8 +530,9 @@ pub const Tutor = struct { }; pub const Help = struct { + pub const output: OutputTraits = .{ .name = config.help_buffer }; pub fn run(c: Ctx) void { - output_pane.openHelp(c.p, c.id, ""); + output_pane.openHelp(c.p, c.id, "") catch |err| c.p.reportError(c.id, "help", err); } }; @@ -488,9 +545,12 @@ pub const Help = struct { // they arm the same tag input `/` does, and Enter runs it (submitSearch). pub const Find = struct { + pub const takes_arg = true; + pub const output: OutputTraits = .{ .name = config.search_buffer, .steps = true }; pub fn run(c: Ctx) void { const pat = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); - if (pat.len > 0) return c.p.runSearch(c.id, pat, .find, .top); + if (pat.len > 0) return c.p.runSearch(c.id, pat, .find, .top) catch |err| + c.p.reportError(c.id, "find", err); c.p.startSearch(c.pane, config.find_marker); } }; @@ -498,9 +558,12 @@ pub const Find = struct { /// Find's sibling: Find matches file NAMES under this pane's directory, Grep /// matches file CONTENTS under every pane's directory at once. pub const Grep = struct { + pub const takes_arg = true; + pub const output: OutputTraits = .{ .name = config.search_buffer, .steps = true }; pub fn run(c: Ctx) void { const pat = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); - if (pat.len > 0) return c.p.runSearch(c.id, pat, .grep, .top); + if (pat.len > 0) return c.p.runSearch(c.id, pat, .grep, .top) catch |err| + c.p.reportError(c.id, "grep", err); c.p.startSearch(c.pane, config.grep_marker); } }; @@ -590,8 +653,9 @@ pub const Last = struct { /// second list kept in step — the buffer is RENDERED from the stack when you /// ask, the way +Search is rendered from a walk. pub const Jumplist = struct { + pub const output: OutputTraits = .{ .name = config.jumps_buffer, .steps = true }; pub fn run(c: Ctx) void { - output_pane.openJumps(c.p, c.id); + output_pane.openJumps(c.p, c.id) catch |err| c.p.reportError(c.id, "jumplist", err); } }; @@ -649,6 +713,7 @@ pub const WsDiagnostics = struct { // no argument arms the tag input and Enter submits (submitSearch). pub const Rename = struct { + pub const takes_arg = true; pub fn run(c: Ctx) void { const a = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); if (a.len > 0) return c.p.lspRequest(c.id, .rename, a); @@ -657,6 +722,7 @@ pub const Rename = struct { }; pub const WsSymbols = struct { + pub const takes_arg = true; pub fn run(c: Ctx) void { const a = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); if (a.len > 0) return c.p.lspRequest(c.id, .workspace_symbols, a); -- cgit v1.3