a73x

e4041d68

build: comptime row lookup for every hand-written module name

a73x   2026-08-14 08:15

Commit message
build: comptime row lookup for every hand-written module name

idx.of's `unreachable` carried a false claim: the fourteen string-literal
call sites (ten named handles, four wasm-section lookups) were NOT
comptime-checked, so a table-row rename would have surfaced as a bare
"reached unreachable code" panic rather than a build error naming the
module. A file-scope idxOf does the lookup at comptime and @compileErrors
on a miss; the literal sites now say `comptime idxOf(...)`, layerOf
collapses onto it, and idx.of survives only for the loop-driven lookups
its corrected comment now describes. Proven by renaming the engine row:
"module table: unknown module 'engine'", pointing at the handle.

Also: the wasm set must be closed under production imports — a .wasm row
importing a row without a twin is now a comptime error instead of the
twin loop's runtime @panic (which stays as belt-and-braces); proven by
dropping protocol's flag. And the ModSpec doc-comment scopes its claim to
table modules, since ghostty and build_options are wired by hand.

Graph unchanged: extract-and-diff against the pre-refactor baseline still
empty. build/test/fmt green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

build.zig
Old New
@@ -73,9 +73,11 @@ fn linkQuic(b: *std.Build, c: *std.Build.Step.Compile, deps: anytype) void {
73 } 73 }
74 74
75 /// One row of the module table: the import graph as declared data. 75 /// One row of the module table: the import graph as declared data.
76 /// The wiring loop below derives every addImport from it, so an import 76 /// The wiring loop below derives every addImport from it, so no
77 /// the table does not declare cannot exist — violations are impossible, 77 /// table-module import can exist except through this loop — violations
78 /// not detected. Layers are the topological strata of the production 78 /// are impossible, not detected. (Dependency edges — ghostty — and
79 /// build_options are explicitly outside the table's jurisdiction and
80 /// stay wired by hand.) Layers are the topological strata of the production
79 /// graph, computed 2026-08-14 and FROZEN: a new import that would 81 /// graph, computed 2026-08-14 and FROZEN: a new import that would
80 /// flatten or invert a stratum fails at comptime, and re-stratifying 82 /// flatten or invert a stratum fails at comptime, and re-stratifying
81 /// requires editing this table, which is the point. 83 /// requires editing this table, which is the point.
@@ -234,13 +236,20 @@ const mod_table = [_]ModSpec{
234 .{ .name = "webhub_main", .path = "src/webhub_main.zig", .layer = 4, .link_libc = true, .imports = &.{ "client", "webhub", "xdg", "handoff", "sockpath" }, .quic_tests = true }, 236 .{ .name = "webhub_main", .path = "src/webhub_main.zig", .layer = 4, .link_libc = true, .imports = &.{ "client", "webhub", "xdg", "handoff", "sockpath" }, .quic_tests = true },
235 }; 237 };
236 238
237 fn layerOf(comptime name: []const u8) u8 { 239 /// Comptime row lookup. Every hand-written module name in this file goes
238 for (mod_table) |m| { 240 /// through it, so renaming a table row fails the build at comptime naming
239 if (std.mem.eql(u8, m.name, name)) return m.layer; 241 /// the module, rather than reaching a runtime `unreachable` in the wiring.
242 fn idxOf(comptime name: []const u8) usize {
243 for (mod_table, 0..) |m, i| {
244 if (std.mem.eql(u8, m.name, name)) return i;
240 } 245 }
241 @compileError("module table: unknown module '" ++ name ++ "'"); 246 @compileError("module table: unknown module '" ++ name ++ "'");
242 } 247 }
243 248
249 fn layerOf(comptime name: []const u8) u8 {
250 return mod_table[idxOf(name)].layer;
251 }
252
244 comptime { 253 comptime {
245 // Both checks are O(edges x rows) name comparisons; the default 1000 254 // Both checks are O(edges x rows) name comparisons; the default 1000
246 // backwards branches does not cover a 32-row, 76-edge table. 255 // backwards branches does not cover a 32-row, 76-edge table.
@@ -256,6 +265,16 @@ comptime {
256 } 265 }
257 // Test-only imports skip the direction rule but must name real rows. 266 // Test-only imports skip the direction rule but must name real rows.
258 for (m.test_imports) |dep| _ = layerOf(dep); 267 for (m.test_imports) |dep| _ = layerOf(dep);
268 // The wasm set must be closed under production imports: a twin can
269 // only import twins, so a row that gains an import from outside the
270 // flagged set fails here rather than at the twin loop's @panic.
271 if (m.wasm) for (m.imports) |dep| {
272 if (!mod_table[idxOf(dep)].wasm) @compileError(std.fmt.comptimePrint(
273 "wasm row {s} imports {s}, which has no .wasm twin — the " ++
274 "wasm set must be closed under production imports",
275 .{ m.name, dep },
276 ));
277 };
259 } 278 }
260 } 279 }
261 280
@@ -314,7 +333,10 @@ pub fn build(b: *std.Build) void {
314 for (&mod_table, 0..) |m, i| { 333 for (&mod_table, 0..) |m, i| {
315 if (std.mem.eql(u8, m.name, name)) return i; 334 if (std.mem.eql(u8, m.name, name)) return i;
316 } 335 }
317 unreachable; // every lookup below is comptime-checked against the table 336 // Loop-driven lookups are comptime-validated by the table checks
337 // above (every imports/test_imports entry names a real row);
338 // named literals use `comptime idxOf` instead of this.
339 unreachable;
318 } 340 }
319 }; 341 };
320 var mods: [mod_table.len]*std.Build.Module = undefined; 342 var mods: [mod_table.len]*std.Build.Module = undefined;
@@ -343,16 +365,16 @@ pub fn build(b: *std.Build) void {
343 // that wiring actually uses (an unused local is a compile error). 365 // that wiring actually uses (an unused local is a compile error).
344 // Dep edges (ghostty) and build_options stay outside the table's 366 // Dep edges (ghostty) and build_options stay outside the table's
345 // jurisdiction, explicit. 367 // jurisdiction, explicit.
346 const engine_mod = mods[idx.of("engine")]; 368 const engine_mod = mods[comptime idxOf("engine")];
347 const mux_mod = mods[idx.of("mux")]; 369 const mux_mod = mods[comptime idxOf("mux")];
348 const exe_mod = mods[idx.of("exe")]; 370 const exe_mod = mods[comptime idxOf("exe")];
349 const muxa_mod = mods[idx.of("muxa")]; 371 const muxa_mod = mods[comptime idxOf("muxa")];
350 const rawmode_mod = mods[idx.of("rawmode")]; 372 const rawmode_mod = mods[comptime idxOf("rawmode")];
351 const delaypipe_mod = mods[idx.of("delaypipe")]; 373 const delaypipe_mod = mods[comptime idxOf("delaypipe")];
352 const render_mod = mods[idx.of("render")]; 374 const render_mod = mods[comptime idxOf("render")];
353 const ptyclient_mod = mods[idx.of("ptyclient")]; 375 const ptyclient_mod = mods[comptime idxOf("ptyclient")];
354 const wsclient_mod = mods[idx.of("wsclient")]; 376 const wsclient_mod = mods[comptime idxOf("wsclient")];
355 const webhub_main_mod = mods[idx.of("webhub_main")]; 377 const webhub_main_mod = mods[comptime idxOf("webhub_main")];
356 378
357 if (ghostty_dep) |dep| { 379 if (ghostty_dep) |dep| {
358 engine_mod.addImport("ghostty-vt", dep.module("ghostty-vt")); 380 engine_mod.addImport("ghostty-vt", dep.module("ghostty-vt"));
@@ -436,15 +458,15 @@ pub fn build(b: *std.Build) void {
436 } 458 }
437 } 459 }
438 } 460 }
439 const engine_wasm_mod = wasm_mods[idx.of("engine")].?; 461 const engine_wasm_mod = wasm_mods[comptime idxOf("engine")].?;
440 if (ghostty_wasm_dep) |dep| { 462 if (ghostty_wasm_dep) |dep| {
441 engine_wasm_mod.addImport("ghostty-vt", dep.module("ghostty-vt")); 463 engine_wasm_mod.addImport("ghostty-vt", dep.module("ghostty-vt"));
442 } 464 }
443 const wasm_core_mod = wasmMod(b, wasm_target, "src/wasm_core.zig"); 465 const wasm_core_mod = wasmMod(b, wasm_target, "src/wasm_core.zig");
444 wasm_core_mod.addImport("engine", engine_wasm_mod); 466 wasm_core_mod.addImport("engine", engine_wasm_mod);
445 wasm_core_mod.addImport("protocol", wasm_mods[idx.of("protocol")].?); 467 wasm_core_mod.addImport("protocol", wasm_mods[comptime idxOf("protocol")].?);
446 wasm_core_mod.addImport("replica", wasm_mods[idx.of("replica")].?); 468 wasm_core_mod.addImport("replica", wasm_mods[comptime idxOf("replica")].?);
447 wasm_core_mod.addImport("keymap", wasm_mods[idx.of("keymap")].?); 469 wasm_core_mod.addImport("keymap", wasm_mods[comptime idxOf("keymap")].?);
448 const wasm_exe = b.addExecutable(.{ .name = "mux_core", .root_module = wasm_core_mod }); 470 const wasm_exe = b.addExecutable(.{ .name = "mux_core", .root_module = wasm_core_mod });
449 // A wasm reactor, not a command: no _start, and the exports must 471 // A wasm reactor, not a command: no _start, and the exports must
450 // survive the linker's dead-strip. Deliberately NOT use_llvm/use_lld — 472 // survive the linker's dead-strip. Deliberately NOT use_llvm/use_lld —