e4041d68
build: comptime row lookup for every hand-written module name
a73x 2026-08-14 08:15
Commit message
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 — |