12 KiB
G-172 Distributed binaries declare a CPU floor - no shipped artifact is tuned to its build host
Status: proposed
Scope: src/buildsuites/suite_tcl90/build905.zig and src/buildsuites/suite_tcl86/build86.zig (flagless target/cpu resolution defaults); src/tools/punkzip/build.zig and src/tools/punkres/build.zig (distributed build-path tools); src/buildsuites/suite_tcl90/tools/family_artifacts.tcl (artifact record emission - the recorded floor field); punkbin artifact repo (external c:/repo/jn/punkbin - win32-x86_64/.toml sidecars, defaults.txt curation); bin/ (the shipped zig-built exes); src/scriptapps/bin/punk-runtime. (floor reporting on fetch, floor-vs-host verdict on use/run); scriptlib/developer/ (cpu-floor audit tool)
Goal: Every binary this project distributes runs on any CPU meeting a declared instruction-set floor - the floor is a property of the build recipe rather than of whichever machine happened to run it, it travels with the artifact in its metadata record, and neither a build nor a publication can silently ship host-tuned code.
Acceptance: A flagless build of each distributed-artifact recipe (suite_tcl90, suite_tcl86, punkzip, punkres) on an AVX-512-capable host emits zero instructions above the declared floor - verified by an audit tool that disassembles the produced binaries and reports out-of-floor instructions, carrying the current AVX-512 regression as its fixture; -Dcpu=native still yields a host-tuned local build, proving the floor is a default and not a restriction; each published artifact's metadata record states the floor it was built to and punk-runtime surfaces it; and selecting or launching a runtime whose recorded floor the local CPU does not meet produces a named diagnosis instead of a silent 0xC000001D, while fetching for any platform stays ungated.
Context
Reported 2026-08-06: on a freshly pulled checkout, bin/punk-runtime.cmd fetch
with no runtime name retrieved the curated default tclsh9.0.5-punk-r2.exe,
which exited immediately with no prompt, no error and no output. Two other
runtimes fetched onto the same machine (tclsh902z.exe, tclsfe-x64.exe)
started normally. The failure was initially read as a Windows-version
difference (25H2 works, 23H2 does not).
It is not an OS difference. Disassembling .text of the artifacts:
| artifact | zmm sites | ymm sites | built by |
|---|---|---|---|
tclsh9.0.5-punk-r2.exe |
2756 | 617 | zig 0.16.0, ReleaseFast |
tclsh9.0.5-r2.exe (plain) |
2756 | 617 | zig 0.16.0, ReleaseFast |
tclsh902z.exe |
0 | 0 | BAWT / MSYS2 mingw GCC |
tclsfe-x64.exe |
0 | 0 | apnadkarni MSVC |
The 9.0.5 binaries carry real EVEX-encoded AVX-512 - vmovdqu64,
vpscatterqq, vpermt2q, vpmovm2q, vptestnmq, vshufi64x2 - with the
first site at RVA 0x2360, the very front of .text. Tcl has no runtime CPU
dispatch, so none of it is guarded. On a CPU without AVX-512 the first one
raises #UD and the process is killed with STATUS_ILLEGAL_INSTRUCTION
(0xC000001D, errorlevel -1073741795) before Tcl writes a byte.
Confirmed against the reporting fleet - the split is exactly the AVX-512 line:
| host | CPU | uarch | AVX-512 | 9.0.5 runtime |
|---|---|---|---|---|
| is-vmhost-1 (Win11 23H2) | TR PRO 3955WX | Zen 2 | no | dies, errorlevel -1073741795 |
| jcross1 (Win11) | Ryzen 5 7600X | Zen 4 | yes | works |
| build/dev box (Win11 25H2) | TR PRO 9955WX | Zen 5 | yes | works |
Cause: the recipes called b.standardTargetOptions(.{}) and no build path
passed -Dtarget/-Dcpu. With no flag, zig resolves cpu_model
determined_by_arch_os by NATIVE detection; on a Zen 5 host that is znver5,
and ReleaseFast then auto-vectorises freely. Measured with the pinned zig on
an auto-vectorisable C loop: flagless -> znver5, zmm=805; -Dcpu=native ->
znver5, zmm=805; -Dtarget=x86_64-windows -> x86_64, zmm=0;
-Dcpu=baseline -> x86_64, zmm=0; -Dcpu=x86_64_v2 -> x86_64_v2, zmm=0.
The class is wider than the runtime family. Every zig-built binary the project distributes inherits its build host's instruction set:
- all six published
tclsh9.0.5-*artifacts (punk + plain, r1 + r2, both-bi-variants): 2756 zmm each - the whole family, so there is no 9.0.5 fallback to fetch instead; bin/punkzip.exe1724 zmm,bin/punkres.exe1423 zmm - both build-path tools, so abakeon a non-AVX-512 machine dies too;bin/punk905.exe,punk9-dev.exe,punk9_beta.exe,punk9bi_beta.exe,punkdeclare.exe,punkfiledemo.exe: 2756 zmm;- an older generation (
punk901*.exe,punk9.exe,xcritcl.exe,tclsh90s*.exe,mkzipfix.exe) carries ~4500 ymm sites and no AVX-512 - AVX2-only, from an earlier build host. Same defect, milder floor.
Two things make this worse than a portability nit. The artifact record already
carries toolchain, optimize and six source-checkout digests but says
nothing about the instruction set, so the one fact that determines whether the
binary can execute is the one fact not recorded. And punkbin defaults.txt
points win32-x86_64 at tclsh9.0.5-punk-r2.exe, so a flagless fetch on a
fresh checkout hands every non-AVX-512 machine a dead default runtime - the
project's first contact with a new user fails with no message at all.
Approach
- Recipe default, not a build-command convention.
standardTargetOptionstakes.{ .default_target = .{ .cpu_model = .baseline } }, so a flagless build is portable and-Dcpu=nativeis the opt-in for a host-tuned local build. "Remember to pass-Dcpu=when publishing" is precisely what failed silently for six artifacts and four recipes; the default is what publishes. (Applied 2026-08-06 to all four recipes - see Progress.) - Floor choice:
baseline(x86-64 v1) for the x86_64 family (user decision 2026-08-06). For a Tcl interpreter the codegen delta against v2/v3 is negligible against the cost of the default runtime failing to start. The win32-ix86 lane (G-130) makes the same choice on its own axis. - Record the floor in the artifact metadata:
[provenance]gains the resolved cpu model and the floor the recipe declared, emitted byfamily_artifacts.tclalongsidetoolchain/optimize, and carried in both the embedded record and the sidecar toml. - Audit tool in
scriptlib/developer/(the advisory-tool tier): disassemble a binary, classify instructions against a named floor, report anything above it. The current AVX-512 artifacts are its fixture - the tool must flag them and must pass a rebuilt one. This is what makes the acceptance criterion measurable rather than a promise about build habits. - punk-runtime surfaces the floor, gating only where execution is implied.
fetchstays ungated and reports the floor as information - fetching for another platform (-platform <p>) is a normal cross-machine workflow and a local-CPU verdict would be meaningless there (user point 2026-08-06). The actionable moment is selection and launch:useandrunare already the local-only actions (runexplicitly takes no-platformbecause "foreign binaries are not runnable here"), so that is where a floor the host does not meet becomes a named diagnosis.listannotates rows the local CPU cannot run. - Republication of the affected artifacts as a new revision, and repointing
punkbin
defaults.txt, is the closing step - sequenced after 1-5 so the rebuilt family is audited and self-describing before it becomes the default.
Alternatives considered
- Pass
-Dcpu=baselineat publish time only, leaving recipes native by default- rejected: it is the same unenforced convention that produced the defect, and it makes a developer's local build and the published artifact differ in a way nothing checks.
x86_64_v2orx86_64_v3floor - deferred, not rejected. v2 (SSE4.2/POPCNT, 2008+) excludes nothing realistically in service; v3 (AVX2/BMI2, 2013+) excludes Intel N-series/Pentium Silver, which have no AVX at all. Revisit per artifact class if a measured win justifies it; the recipe already accepts-Dcpu=<model>.- Runtime CPU dispatch in the hot paths - rejected as out of proportion: Tcl upstream has none, and the goal is that a shipped binary starts everywhere, not that it is optimal everywhere.
- Detect and report at first run instead of building portably - rejected: the
failure is #UD before
main, so there is no point at which our own code could report anything.
Notes
- Related: G-105 - cross-target builds parameterize the same recipe's target; this goal is the CPU axis of that surface. Whichever lands first, the other inherits the resolved default_target shape.
- Related: G-130 - the win32-ix86 lane consumes the floor decision on its own axis (an i686 baseline floor is the same class of choice).
- Related: G-116, G-108 - additional products of the same recipe (tcltls
bi-family battery, the debug
-dbg<n>tier); both inherit the pin. - Related: G-142 - curated listing manifests are where a per-artifact floor becomes visible before download.
- Related: G-147 - fetch-side sibling; a floor field joins the facts a revision check reads.
- Related: G-137 - punkres RT_VERSION stamping consumes
bin/punkres.exe, itself one of the affected binaries. - Related (archived): G-117 (self-describing runtimes - see goals/archive/G-117-self-describing-runtimes.md) and G-123 (runtime tiers, schema v2 - see goals/archive/G-123-thirdparty-runtime-tiers.md) - the artifact record schema this goal extends with the floor field.
- Related (archived): G-126 (punkzip accelerator), G-128 (punkres stamper) - the two distributed tools whose recipes carry the same defect.
- Related (archived): G-102 - the suite zig-version pin lives in the same comptime block as the defect.
- Overlap survey 2026-08-06 (
goals_xref.tcl paths src/buildsuites/suite_tcl90 bin/punkzip.exe bin/punkres.exe): surveyed and judged not related in substance - G-110 (shared-lib extraction cache), G-131 (boot payload autodetection), G-141, G-157, G-158 (bin/ sibling-file matches only). - Deliberately outside this contract: all four recipes gate on
required_zig = "0.16.0"with a.ltSemanticVersion compare, so a prerelease toolchain (0.16.0-dev.254+6dd0270a1, the copy under bin/tools) sorts below the pin and is rejected by the recipes' own gate. A real defect in the same lines, but a different one - it blocks rebuilding here until a released 0.16.0 is materialized. - Verification note:
llvm-objdump -d --section=.text <exe>countingzmm[0-9]operands is the quick manual check used throughout the Context measurements. Linear disassembly of a non-AVX-512 binary yields ~3 false positives from data misparse; real usage is in the hundreds-to-thousands, so the distinction is unambiguous at a glance but the audit tool (Approach 4) should decode properly rather than grep.
Progress
- 2026-08-06: recipe pins applied (Approach 1+2) -
default_targetwithcpu_model = .baselineinsrc/buildsuites/suite_tcl90/build905.zig,src/buildsuites/suite_tcl86/build86.zig,src/tools/punkzip/build.zigandsrc/tools/punkres/build.zig, each carrying the finding in-comment. Verified byzig ast-checkon all four; the mechanism verified separately with the same toolchain and optimize level on an equivalent recipe (flagless -> baseline/zmm=0,-Dcpu=native-> znver5/zmm=805). A full suite build was NOT run - blocked by the version-gate defect noted above. Theb.graph.hosttarget sites elsewhere in these recipes are build-time helper tools that are never distributed and are correct as native. - Remaining for acceptance: Approach 3 (floor in the artifact record), 4 (audit
tool + fixture), 5 (punk-runtime surfacing and the use/run verdict), 6
(rebuild, republish, repoint
defaults.txt).