diff --git a/AGENTS.md b/AGENTS.md index 2fcfc5b8..653a91cf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -214,7 +214,7 @@ The project version is fully independent of module versions. A module bump (even - `TEMP_REFERENCE/` - Reference-only material for agents, not a work folder; ignored by both git and fossil, so its contents (including its AGENTS.md) are machine-local and unversioned. Holds the nested fossil checkout of the Tcl core sources at `TEMP_REFERENCE/tcl9`, which must stay on `trunk` per the fossil branch-discipline rule in User Preferences (see TEMP_REFERENCE/AGENTS.md when present) - Directories agents should not directly modify (no child DOX needed): - `callbacks/` — Experimental shellspy features, user-only - - `scriptlib/` — Shared utilities + manual tests, user-only. EXCEPTIONS: `scriptlib/_punktest/` is test-owned (fixtures for `src/tests/shell/testsuites/punkexe/scriptexec.test`, resolved via `lib:_punktest/`); agents may manage that subfolder as part of test work. `scriptlib/developer/` holds agent-authored developer showcase/demo apps and developer utility scripts (created at user request 2026-07-11, e.g `tkconsole_demo.tcl` for the G-001 tk console backend, `goals_lint.tcl` for the goals doc contracts per goals/AGENTS.md Verification, `goals_xref.tcl` goals cross-reference/scope-overlap analysis (unlinked overlap pairs, path-intersection queries, dangling refs) per goals/AGENTS.md Verification, `whatis.tcl` runtime command introspection backing the `tcl-whatis` agent skill, `nslist.tcl` runtime namespace/package discovery backing the `tcl-nslist` agent skill, `runtests_parity.tcl` comparing `runtests.tcl -report json` outputs for result parity between runner modes per src/tests/AGENTS.md Verification, `architecture_lint.tcl` validating root `ARCHITECTURE.md` references - path existence, goal-id resolution, size budget, `assetorigin_check.tcl` classifying assets against their `*.assetorigin.toml` provenance sidecars per the format in `src/assets/logo/AGENTS.md` - six advisory states, fixture selftest, plain tclsh); agents may add or update entries there when the user asks for one. The rest of `scriptlib/` stays user-only. + - `scriptlib/` — Shared utilities + manual tests, user-only. EXCEPTIONS: `scriptlib/_punktest/` is test-owned (fixtures for `src/tests/shell/testsuites/punkexe/scriptexec.test`, resolved via `lib:_punktest/`); agents may manage that subfolder as part of test work. `scriptlib/developer/` holds agent-authored developer showcase/demo apps and developer utility scripts (created at user request 2026-07-11, e.g `tkconsole_demo.tcl` for the G-001 tk console backend, `goals_lint.tcl` for the goals doc contracts per goals/AGENTS.md Verification, `goals_xref.tcl` goals cross-reference/scope-overlap analysis (unlinked overlap pairs, path-intersection queries, dangling refs) per goals/AGENTS.md Verification, `whatis.tcl` runtime command introspection backing the `tcl-whatis` agent skill, `nslist.tcl` runtime namespace/package discovery backing the `tcl-nslist` agent skill, `runtests_parity.tcl` comparing `runtests.tcl -report json` outputs for result parity between runner modes per src/tests/AGENTS.md Verification, `architecture_lint.tcl` validating root `ARCHITECTURE.md` references - path existence, goal-id resolution, size budget, `assetorigin_check.tcl` classifying assets against their `*.assetorigin.toml` provenance sidecars per the format in `src/assets/logo/AGENTS.md` - six advisory states, fixture selftest, plain tclsh, `cpufloor_audit.tcl` the G-172 distributed-binary CPU-floor audit instrument (disassembles a binary's .text via a located objdump, or audits a pre-disassembled excerpt, and reports AVX-register-width instructions above a declared floor; bundled real-disassembly fixtures in `scriptlib/developer/cpufloor_fixtures/` + a `-selftest`); agents may add or update entries there when the user asks for one. The rest of `scriptlib/` stays user-only. - `bin/` — Built binaries and helpers, build output target. This includes the polyglot `.cmd` launcher/utility scripts (e.g `bin/punk-runtime.cmd`): they are GENERATED by the punk::mix scriptwrap machinery from sources under `src/scriptapps/` — a request to "fix bin/.cmd" means editing `src/scriptapps/.*` + `_wrap.toml` and re-wrapping (see bin/AGENTS.md), never editing the output - `modules/` (root) — Build output target for `tclsh src/make.tcl modules` - `lib/` (root) — Build output target for `tclsh src/make.tcl libs` diff --git a/CHANGELOG.md b/CHANGELOG.md index a6b81885..521dab0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ The latest `## [X.Y.Z]` header must match the `version` field in `punkproject.to Entries are newest-first; one bullet per notable change. See the root `AGENTS.md` "Project Versioning" section for the bump policy. +## [0.57.1] - 2026-08-07 + +- punk-runtime surfaces a recorded CPU instruction-set floor and gates launching a + runtime the local CPU cannot run (G-172). A runtime's artifact metadata toml + now carries `cpu_floor` (the recipe's declared default floor) and `cpu_model` + (the build's resolved CPU model); `fetch` reports them (ungated - a + cross-platform fetch for another machine stays free), `list` annotates rows the + host cannot run (`!CPU-FLOOR:v`), `use`/`run` abort with a named diagnosis + instead of a silent STATUS_ILLEGAL_INSTRUCTION (0xC000001D), and `info` shows + the fields. The suite_tcl90 kit-family record writes the floor into each + artifact's `[provenance]`, and a new codegen audit tool + `scriptlib/developer/cpufloor_audit.tcl` disassembles a binary and reports + instructions above the declared floor (the acceptance instrument for the + AVX-512 host-tuning regression). Floor-bearing published artifacts land at + the next republication. + ## [0.57.0] - 2026-08-06 - Recognised-but-unavailable documentation landings are now surfaced distinctly diff --git a/GOALS.md b/GOALS.md index c75a84ec..8751c70f 100644 --- a/GOALS.md +++ b/GOALS.md @@ -406,7 +406,7 @@ Detail: goals/G-167-punkargs-version-delta-availability.md Scope: src/make.tcl (bake/bakehouse/vfslibs - VFSPAYLOAD + PACKAGES phases, kit assembly, new explicit materialize/tidy commands); src/make.tcl sync_layouts phase (the mint-side tracked-path write) with its targets src/project_layouts/ and src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/ plus the workflow_text TERMINOLOGY/DIAGRAM 1b stage-verb assignment; src/vfs/ (payload declarations, _targets staging generalization, README.md + AGENTS.md precedence spec); src/_bake (assembly staging); bin/packages (lib-tier consumption); .gitignore + .fossil-settings/ignore-glob (staging ignore coverage); src/tests/shell/testsuites/punkexe/ (kill-probe + precedence characterization) Detail: goals/G-171-bake-writes-land-untracked.md -### G-172 [proposed] Distributed binaries declare a CPU floor - no shipped artifact is tuned to its build host +### G-172 [active] Distributed binaries declare a CPU floor - no shipped artifact is tuned to its build host 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) Detail: goals/G-172-distributed-binary-cpu-floor.md diff --git a/bin/punk-runtime.cmd b/bin/punk-runtime.cmd index 3761066f..600a4564 100755 --- a/bin/punk-runtime.cmd +++ b/bin/punk-runtime.cmd @@ -1527,6 +1527,76 @@ provenance_class() { } ' } +#G-172 CPU instruction-set floor surfacing and runnability verdict (ps1 +#payload parity). A runtime's toml carries cpu_floor (the recipe's declared +#default floor) and cpu_model (what the build resolved to). host_cpu_level detects +#the local CPU's x86-64 level from /proc/cpuinfo flags (avx512f=v4, avx2=v3, +#sse4_2=v2, sse2=v1); cpu_floor_verdict compares and sets globals so use/run gate +#launching a runtime the local CPU cannot run (a named diagnosis instead of a +#silent STATUS_ILLEGAL_INSTRUCTION 0xC000001D - the 2026-08-06 AVX-512 failure). +#A detection gap (no /proc/cpuinfo) yields level 0 and the verdict never blocks +#on unknown, so a detection failure cannot prevent a launch. +punk_host_cpu_level="" +host_cpu_level() { + [[ -n "$punk_host_cpu_level" ]] && { echo "$punk_host_cpu_level"; return; } + local lvl=0 flags + if [[ -r /proc/cpuinfo ]]; then + flags=$(grep -m1 '^flags' /proc/cpuinfo 2>/dev/null || true) + if printf '%s' "$flags" | grep -qw 'avx512f'; then lvl=4 + elif printf '%s' "$flags" | grep -qw 'avx2'; then lvl=3 + elif printf '%s' "$flags" | grep -qw 'sse4_2'; then lvl=2 + elif printf '%s' "$flags" | grep -qw 'sse2'; then lvl=1 + fi + fi + punk_host_cpu_level=$lvl + echo "$lvl" +} +#G-172 map a recorded cpu_model name to an x86-64 level (1..4), or 0 for a +#specific uarch / unknown. A specific uarch (znver5, skylake...) is NOT a level +#- the codegen audit tool (scriptlib/developer/cpufloor_audit.tcl) is the +#authoritative runnability check for those. +cpu_model_level() { + case "$1" in + x86_64) echo 1;; + baseline) echo 1;; + x86_64_v2) echo 2;; + x86_64_v3) echo 3;; + x86_64_v4) echo 4;; + *) echo 0;; + esac +} +#G-172 floor-vs-host verdict. $1 = path to the runtime's toml. Sets globals: +#floor_verdict (meets|below|unknown|norecord), floor_required, floor_host, +#floor_message, floor_cpu_floor, floor_cpu_model. 'below' => recorded codegen +#exceeds the host - the caller MUST NOT launch (would die with 0xC000001D). +#'unknown' => specific uarch or host undetermined: do not block, surface the audit. +cpu_floor_verdict() { + local toml="$1" + floor_cpu_floor=$(sed -n 's/^[[:space:]]*cpu_floor[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$toml" 2>/dev/null | head -n 1) + floor_cpu_model=$(sed -n 's/^[[:space:]]*cpu_model[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$toml" 2>/dev/null | head -n 1) + floor_host=$(host_cpu_level) + floor_required=0; floor_message="" + if [[ -z "$floor_cpu_model" ]]; then + floor_verdict="norecord"; return + fi + floor_required=$(cpu_model_level "$floor_cpu_model") + if [[ "$floor_required" -eq 0 ]]; then + floor_verdict="unknown" + floor_message="cpu_model='$floor_cpu_model' is a specific uarch (not an x86-64 level); runnability is verified by the codegen audit tool: tclsh scriptlib/developer/cpufloor_audit.tcl (declared floor: $floor_cpu_floor)" + return + fi + if [[ "$floor_host" -eq 0 ]]; then + floor_verdict="unknown" + floor_message="host CPU level undetermined (/proc/cpuinfo unavailable); recorded cpu_model='$floor_cpu_model' (x86-64 v$floor_required), floor='$floor_cpu_floor' - runnability not verified" + return + fi + if [[ "$floor_host" -lt "$floor_required" ]]; then + floor_verdict="below" + floor_message="runtime built for cpu_model='$floor_cpu_model' (x86-64 v$floor_required) but this CPU is x86-64 v$floor_host - launching it would fail with STATUS_ILLEGAL_INSTRUCTION (0xC000001D). Aborting; use '$0 list' for a runtime this CPU can run." + return + fi + floor_verdict="meets" +} #G-103 artifact metadata: a runtime may carry a .toml beside it (emitted #by the buildsuite kit-family-artifacts step / fetched from punkbin). Prints a #short "[variant=... tcl=... rN ...]" summary for list output, "" when absent. @@ -1554,6 +1624,24 @@ metadata_summary() { #integrity flag: a runtime filed under a platform folder its metadata says it #was not built for (cross-platform fetch/staging misfiling) [[ -n "$target" && "$target" != "$archtail" ]] && parts="$parts !TARGET-MISMATCH:$target" + #G-172 CPU floor surface + runnability annotation. floor= is informational; a + #!CPU-FLOOR:v tag marks a row the local CPU cannot run - gated to the + #LOCAL platform only (a foreign-platform row is cross-build staging). Absent + #on pre-G-172 tomls. (runs in a command-substitution subshell - host_cpu_level + #caches only within this call, which is fine.) + local cfloor cmodel cmlevel hlevel + cfloor=$(sed -n 's/^[[:space:]]*cpu_floor[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$tomlfile" 2>/dev/null | head -n 1) + [[ -n "$cfloor" ]] && parts="$parts floor=$cfloor" + if [[ "$archtail" == "$local_platform" ]]; then + cmodel=$(sed -n 's/^[[:space:]]*cpu_model[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$tomlfile" 2>/dev/null | head -n 1) + if [[ -n "$cmodel" ]]; then + cmlevel=$(cpu_model_level "$cmodel") + if [[ "$cmlevel" -gt 0 ]]; then + hlevel=$(host_cpu_level) + [[ "$hlevel" -gt 0 && "$hlevel" -lt "$cmlevel" ]] && parts="$parts !CPU-FLOOR:v$cmlevel" + fi + fi + fi [[ -n "$parts" ]] && printf '[%s]' "${parts# }" } #operator help (the 'help' action; the no-args case shows show_usage only). @@ -1911,6 +1999,12 @@ case "$action" in #build-origin class (table-aware read - schema v2) fclass=$(provenance_class < "${archdir}/${tomlname}") [[ -n "$fclass" ]] && echo "provenance: class=$fclass" + #G-172: surface the recorded CPU floor as information. Fetch is UNGATED + #- a cross-platform fetch (-platform

) for another machine is normal + #and a local-CPU verdict would be meaningless there. + ffloor=$(sed -n 's/^[[:space:]]*cpu_floor[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "${archdir}/${tomlname}" 2>/dev/null | head -n 1) + fmodel=$(sed -n 's/^[[:space:]]*cpu_model[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "${archdir}/${tomlname}" 2>/dev/null | head -n 1) + [[ -n "$ffloor" || -n "$fmodel" ]] && echo "cpu floor: floor=$ffloor model=$fmodel" else rm -f "${archdir}/${tomlname}" echo "no artifact metadata toml on server for $runtime (ok for pre-family runtimes)" @@ -2154,6 +2248,21 @@ case "$action" in done exit 1 fi + #G-172 floor verdict: selecting a runtime the local CPU cannot run is a + #named diagnosis instead of a silent 0xC000001D on the next 'run'. Only + #the LOCAL platform is gated - a foreign -platform folder is cross-build + #staging deployed elsewhere to run. 'below' blocks the selection. + if [[ "$platform" == "$local_platform" ]]; then + utoml="$archdir/$(rootname_of "$1").toml" + if [[ -f "$utoml" ]]; then + cpu_floor_verdict "$utoml" + if [[ "$floor_verdict" == "below" ]]; then + echo "punk-runtime use: $1 - $floor_message" + exit 1 + fi + [[ "$floor_verdict" == "unknown" ]] && echo "punk-runtime use: $1 - $floor_message" + fi + fi #G-103 artifact-tier names (-r, immutable): 'use' MATERIALIZES the #artifact into its WORKING name (name minus -r - what mapvfs and #projects reference), copies its metadata toml alongside, and selects @@ -2222,6 +2331,20 @@ case "$action" in fi fi activeruntime_fullpath="$archdir/$activeruntime" + #G-172 floor verdict: launching a runtime the local CPU cannot run is a + #named diagnosis instead of a silent 0xC000001D (#UD before main - the + #2026-08-06 AVX-512 regression failure mode). 'below' aborts before the + #launch; 'unknown' notes the audit tool but does not block (a -Dcpu=native + #local build runs on its own host by definition). + rtoml="$archdir/$(rootname_of "$activeruntime").toml" + if [[ -f "$rtoml" ]]; then + cpu_floor_verdict "$rtoml" + if [[ "$floor_verdict" == "below" ]]; then + echo "punk-runtime run: $activeruntime - $floor_message" + exit 1 + fi + [[ "$floor_verdict" == "unknown" ]] && echo "punk-runtime run: $activeruntime - $floor_message" + fi #echo "using $activeruntime_fullpath" #(the action was already shifted off during the option scan - "$@" is #exactly the runtime's argument list) @@ -2350,7 +2473,7 @@ case "$action" in if [[ -n "$embedded" || -f "$sidecar" ]]; then printf ' %-20s %-36s %s\n' "field" "embedded" "sidecar" disagreements="" - for k in schema name class variant working_name revision target build_id origin packager builder source_url upstream_ref retrieved project project_url license build_host_platform tcl_patchlevel sha1 size built; do + for k in schema name class variant working_name revision target build_id origin packager builder source_url upstream_ref retrieved project project_url license build_host_platform tcl_patchlevel cpu_floor cpu_model sha1 size built; do ev=""; sv="" if [[ -n "$embedded" ]]; then ev=$(printf '%s\n' "$embedded" | sed -n 's/^[[:space:]]*'"$k"'[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' | head -n 1) @@ -3035,6 +3158,22 @@ function Get-PunkRuntimeMetadataSummary { if ($expectedplatform -ne "" -and $fields.ContainsKey('target') -and $fields['target'] -ne $expectedplatform) { $parts += "!TARGET-MISMATCH:$($fields['target'])" } + #G-172 CPU floor surface + runnability annotation. floor= is informational (the + #recipe's declared default); !CPU-FLOOR:v marks a row the local CPU cannot + #run - gated to the LOCAL platform only (a foreign-platform row is cross-build + #staging, not runnable here regardless). Absent on pre-G-172 tomls (no tag). + if ($fields.ContainsKey('cpu_floor') -and $fields['cpu_floor'] -ne "") { + $parts += "floor=$($fields['cpu_floor'])" + } + if ($expectedplatform -eq $script:PunkLocalPlatform -and $fields.ContainsKey('cpu_model') -and $fields['cpu_model'] -ne "") { + $cmLevel = Get-PunkCpuModelLevel $fields['cpu_model'] + if ($cmLevel -gt 0) { + $hLevel = Get-PunkHostCpuLevel + if ($hLevel -gt 0 -and $hLevel -lt $cmLevel) { + $parts += "!CPU-FLOOR:v$cmLevel" + } + } + } if ($parts.Count -eq 0) { return "" } @@ -3079,6 +3218,99 @@ function Get-PunkRuntimeProvenanceClass { } return "" } +#G-172 CPU instruction-set floor surfacing and runnability verdict. A runtime's +#metadata toml (and embedded record) carry cpu_floor (the recipe's declared default +#floor) and cpu_model (what the build ACTUALLY resolved to). A published artifact +#built at the recipe default records cpu_model="x86_64" (baseline, v1); -Dcpu=native +#honestly records the host uarch. The verdict compares the recorded codegen level +#to the local CPU so selecting/launching a runtime the local CPU cannot run is a +#NAMED diagnosis instead of a silent STATUS_ILLEGAL_INSTRUCTION (0xC000001D) - +#the failure mode of the 2026-08-06 AVX-512 regression (see G-172 Context). Fetch +#stays ungated (a cross-platform fetch for another machine is normal); only the +#local-only actions (use/run) gate, and list annotates rows the host cannot run. +#Host level detection: IsProcessorFeaturePresent (kernel32) with the documented +#PF constants (winnt.h): SSE2=10 (v1 baseline), SSE4.2=38 (v2), AVX2=40 (v3), +#AVX512F=41 (v4). Detected once and cached; a detection failure degrades to 0 +#(unknown) and the verdict never blocks on unknown, so a detection gap cannot +#prevent a launch - only a POSITIVE below-floor verdict blocks. +$script:PunkHostCpuLevel = $null +function Get-PunkHostCpuLevel { + if ($null -ne $script:PunkHostCpuLevel) { return $script:PunkHostCpuLevel } + $code = @' +using System; +using System.Runtime.InteropServices; +public class PunkCpuFeat { + [DllImport("kernel32.dll")] + static extern bool IsProcessorFeaturePresent(uint feature); + public static int Level() { + if (IsProcessorFeaturePresent(41)) return 4; //AVX512F (x86-64 v4) + if (IsProcessorFeaturePresent(40)) return 3; //AVX2 (x86-64 v3) + if (IsProcessorFeaturePresent(38)) return 2; //SSE4.2 (x86-64 v2) + if (IsProcessorFeaturePresent(10)) return 1; //SSE2 (x86-64 v1 baseline) + return 0; + } +} +'@ + try { + Add-Type -TypeDefinition $code -ErrorAction Stop + $script:PunkHostCpuLevel = [PunkCpuFeat]::Level() + } catch { + #P/Invoke unavailable (locked-down host / broken PS): degrade to unknown + $script:PunkHostCpuLevel = 0 + } + return $script:PunkHostCpuLevel +} +#G-172 map a recorded cpu_model name to an x86-64 level (1..4), or 0 for a +#specific uarch / unknown name. The standard zig level models map directly; a +#specific uarch (znver5, skylake, ...) is NOT a level - the codegen audit tool +#(scriptlib/developer/cpufloor_audit.tcl) is the authoritative runnability check +#for those, since punk-runtime cannot map a uarch name to a feature set. +function Get-PunkCpuModelLevel { + param([string] $cpuModel) + switch ($cpuModel) { + "x86_64" { return 1 } + "baseline" { return 1 } + "x86_64_v2" { return 2 } + "x86_64_v3" { return 3 } + "x86_64_v4" { return 4 } + default { return 0 } + } +} +#G-172 floor-vs-host verdict. $fields is the record-field hashtable from +#Get-PunkRuntimeRecordFields on the runtime's toml. Returns a hashtable: +# Verdict 'meets' | 'below' | 'unknown' | 'norecord' +# Required 0..4 (the recorded codegen level; 0 = unknown/specific-uarch) +# Host 0..4 (the local CPU level; 0 = undetermined) +# Message the diagnosis line (empty on meets/norecord) +# CpuFloor / CpuModel as recorded (empty if absent) +#'below' => the recorded codegen exceeds the host: launching would die with +#0xC000001D, so the caller MUST NOT launch. 'unknown' => cannot verdict by name +#(specific uarch or host undetermined): do not block, surface the audit tool. +function Get-PunkCpuFloorVerdict { + param([hashtable] $fields) + $cm = ""; if ($fields.ContainsKey('cpu_model')) { $cm = $fields['cpu_model'] } + $cf = ""; if ($fields.ContainsKey('cpu_floor')) { $cf = $fields['cpu_floor'] } + #NOTE: deliberately NOT named $host - that is a read-only automatic variable + #in PowerShell (the $Host object); reusing it throws VariableNotWritable. + $hlevel = Get-PunkHostCpuLevel + if ($cm -eq "") { + return @{ Verdict='norecord'; Required=0; Host=$hlevel; Message=""; CpuFloor=$cf; CpuModel=$cm } + } + $req = Get-PunkCpuModelLevel $cm + if ($req -eq 0) { + $msg = "cpu_model='$cm' is a specific uarch (not an x86-64 level); runnability is verified by the codegen audit tool: tclsh scriptlib/developer/cpufloor_audit.tcl (declared floor: $cf)" + return @{ Verdict='unknown'; Required=0; Host=$hlevel; Message=$msg; CpuFloor=$cf; CpuModel=$cm } + } + if ($hlevel -eq 0) { + $msg = "host CPU level undetermined (IsProcessorFeaturePresent unavailable); recorded cpu_model='$cm' (x86-64 v$req), floor='$cf' - runnability not verified" + return @{ Verdict='unknown'; Required=$req; Host=0; Message=$msg; CpuFloor=$cf; CpuModel=$cm } + } + if ($hlevel -lt $req) { + $msg = "runtime built for cpu_model='$cm' (x86-64 v$req) but this CPU is x86-64 v$hlevel - launching it would fail with STATUS_ILLEGAL_INSTRUCTION (0xC000001D). Aborting; use 'punk-runtime list' for a runtime this CPU can run." + return @{ Verdict='below'; Required=$req; Host=$hlevel; Message=$msg; CpuFloor=$cf; CpuModel=$cm } + } + return @{ Verdict='meets'; Required=$req; Host=$hlevel; Message=""; CpuFloor=$cf; CpuModel=$cm } +} #G-117 embedded record: zip CENTRAL-DIRECTORY read of the exe-appended archive via #.NET System.IO.Compression (offsets resolve from the end-of-central-directory #record, so the prepended executable data is no obstacle). The target is NEVER @@ -3614,6 +3846,16 @@ function psmain { #build-origin class (table-aware read - schema v2) $fclass = Get-PunkRuntimeProvenanceClass ([string[]](Get-Content -Path $tomllocal)) if ($fclass -ne "") { Write-Host "provenance: class=$fclass" } + #G-172: surface the recorded CPU floor as information. Fetch is + #UNGATED - a cross-platform fetch (-platform

) for another + #machine is a normal workflow and a local-CPU verdict would be + #meaningless there, so only the recorded floor/model are printed. + $fflds = Get-PunkRuntimeRecordFields ([string[]](Get-Content -Path $tomllocal)) + $ffloor = ""; if ($fflds.ContainsKey('cpu_floor')) { $ffloor = $fflds['cpu_floor'] } + $fmodel = ""; if ($fflds.ContainsKey('cpu_model')) { $fmodel = $fflds['cpu_model'] } + if ($ffloor -ne "" -or $fmodel -ne "") { + Write-Host "cpu floor: floor=$ffloor model=$fmodel" + } } catch { Write-Host "no artifact metadata toml on server for $runtime (ok for pre-family runtimes)" } @@ -3687,6 +3929,25 @@ function psmain { } exit 1 } + #G-172 floor verdict: selecting a runtime the local CPU cannot run is a + #named diagnosis instead of a silent 0xC000001D on the next 'run'. + #Only the LOCAL platform is gated - a foreign -platform folder is + #cross-build staging deployed elsewhere to run. 'below' blocks the + #selection; 'unknown' notes the audit tool but does not block. + if ($arch -eq $script:PunkLocalPlatform) { + $utoml = Join-Path -Path $archfolder -ChildPath ((Get-PunkRuntimeRootName $rtname) + ".toml") + if (Test-Path -Path $utoml -PathType Leaf) { + $uflds = Get-PunkRuntimeRecordFields ([string[]](Get-Content -Path $utoml)) + $uverd = Get-PunkCpuFloorVerdict $uflds + if ($uverd.Verdict -eq 'below') { + Write-Host "punk-runtime use: $rtname - $uverd.Message" + exit 1 + } + if ($uverd.Verdict -eq 'unknown') { + Write-Host "punk-runtime use: $rtname - $uverd.Message" + } + } + } #G-103 artifact-tier names (-r, immutable): 'use' MATERIALIZES the #artifact into its WORKING name (name minus -r - what mapvfs and #projects reference), copies its metadata toml alongside, and selects @@ -3762,6 +4023,24 @@ function psmain { } $active = Join-Path -Path $archfolder -ChildPath $activename write-host "using: $active" + #G-172 floor verdict: launching a runtime the local CPU cannot run + #is a named diagnosis instead of a silent 0xC000001D (#UD before + #main - the failure mode of the 2026-08-06 AVX-512 regression). + #'below' aborts before Start-Process; 'unknown' notes the audit + #tool but does not block (a -Dcpu=native local build runs on + #its own host by definition). + $rtoml = Join-Path -Path $archfolder -ChildPath ((Get-PunkRuntimeRootName $activename) + ".toml") + if (Test-Path -Path $rtoml -PathType Leaf) { + $rflds = Get-PunkRuntimeRecordFields ([string[]](Get-Content -Path $rtoml)) + $rverd = Get-PunkCpuFloorVerdict $rflds + if ($rverd.Verdict -eq 'below') { + Write-Host "punk-runtime run: $activename - $rverd.Message" + exit 1 + } + if ($rverd.Verdict -eq 'unknown') { + Write-Host "punk-runtime run: $activename - $rverd.Message" + } + } if ($PSBoundParameters.opts.Length -gt 0) { $optsType = $PSBoundParameters.opts.GetType() #method can only be called if .opts is not null write-host "type of opts: $($optsType.FullName)" @@ -4114,7 +4393,7 @@ function psmain { if ($havesidecar) { $sfields = Get-PunkRuntimeRecordFields ([string[]](Get-Content -Path $sidecarfile)) } write-host (" {0,-20} {1,-36} {2}" -f "field", "embedded", "sidecar") $disagreements = @() - foreach ($k in @('schema','name','class','variant','working_name','revision','target','build_id','origin','packager','builder','source_url','upstream_ref','retrieved','project','project_url','license','build_host_platform','tcl_patchlevel','sha1','size','built')) { + foreach ($k in @('schema','name','class','variant','working_name','revision','target','build_id','origin','packager','builder','source_url','upstream_ref','retrieved','project','project_url','license','build_host_platform','tcl_patchlevel','cpu_floor','cpu_model','sha1','size','built')) { $ev = ""; $sv = "" if ($efields.ContainsKey($k)) { $ev = $efields[$k] } if ($sfields.ContainsKey($k)) { $sv = $sfields[$k] } diff --git a/goals/G-172-distributed-binary-cpu-floor.md b/goals/G-172-distributed-binary-cpu-floor.md index ac08d19b..d241e049 100644 --- a/goals/G-172-distributed-binary-cpu-floor.md +++ b/goals/G-172-distributed-binary-cpu-floor.md @@ -1,6 +1,6 @@ # G-172 Distributed binaries declare a CPU floor - no shipped artifact is tuned to its build host -Status: proposed +Status: active 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. @@ -220,3 +220,68 @@ project's first contact with a new user fails with no message at all. fixture), 5 (punk-runtime surfacing and the use/run verdict). Behind those: r1-generation disposition (replace vs drop) and the bin/ punk9* kit rebake with the new runtime (punkproject version-gate discipline applies). +- 2026-08-07: goal activated at user direction (overlap survey re-run via + `goals_xref.tcl score G-172` - no goals drafted in the interval; the existing + Related: notes cover every overlapping pair). Approaches 3, 4, 5 CODE landed: + + - Approach 3 (record floor field): `src/buildsuites/suite_tcl90/build905.zig` + declares `const cpu_floor = "baseline";` beside the default_target pin and + writes `cpu_floor` + `cpu_model` (=`target.result.cpu.model.name`, resolving + to "x86_64" for the default build, "znver5" for -Dcpu=native) into the + embedded record's [provenance], after toolchain/optimize (ast-check PASS; + 17 format placeholders = 17 args). `tools/family_artifacts.tcl` derives both + from the embedded record (G-117 single-source-of-truth, like + origin/packager) and emits them into the sidecar [provenance], failing if + absent. `tools/family_check.tcl` asserts embed_cpu_floor/embed_cpu_model + non-empty. suite_tcl86 has no family machinery (G-158 territory) - only the + build86.zig cpu_model pin (already in place). NOT yet built/published - the + floor field reaches published artifacts at the next republication. + + - Approach 4 (audit tool): `scriptlib/developer/cpufloor_audit.tcl` (plain + tclsh, no deps) disassembles a binary's .text via a located objdump + (llvm-objdump preferred, GNU objdump fallback) or audits a pre-disassembled + excerpt (`-from-dis`), classifies by AVX register width (zmm=v4, ymm=v3; + baseline/v2 forbid both, v3 forbids zmm, v4 allows all - SSE4.2 not + width-detectable so baseline and v2 share a ceiling), reports out-of-floor + count + first RVA + sample mnemonics, exit 0 PASS / 1 FAIL. Selftest + (`-selftest`) runs 12 fixture/floor cases - all pass. Fixtures are REAL + disassembly excerpts in `scriptlib/developer/cpufloor_fixtures/` (avx512, + avx2, clean) generated from a temp-built probe on this Zen 5 host (no + binary committed). Verified against real binaries: rebuilt bin/punkzip.exe + PASS (zmm=0 ymm=0); a -Dcpu=native probe FAILs at baseline (123 zmm, 151 + out-of-floor, first RVA 140002952) and PASSes at v4. The acceptance's + measurable instrument is in place. + + - Approach 5 (punk-runtime surfacing): both payload twins updated and + re-wrapped. `src/scriptapps/bin/punk-runtime.ps1` adds + `Get-PunkHostCpuLevel` (IsProcessorFeaturePresent via Add-Type P/Invoke; + PF SSE2=10/SSE4.2=38/AVX2=40/AVX512F=41 -> level 1..4, cached, 0 on + detection failure), `Get-PunkCpuModelLevel` (x86_64/baseline->1, _v2->2, + _v3->3, _v4->4, else 0), `Get-PunkCpuFloorVerdict` (meets|below|unknown| + norecord; below => block). `punk-runtime.bash` mirrors with + `host_cpu_level` (/proc/cpuinfo flags avx512f/avx2/sse4_2/sse2), + `cpu_model_level`, `cpu_floor_verdict` (sets globals). Wired: fetch prints + `cpu floor: floor=.. model=..` (UNGATED - cross-platform fetch stays free); + list metadata_summary adds `floor=` and `!CPU-FLOOR:v` (local-platform + rows only); use gates on the local platform (below => exit 1, named + diagnosis); run gates before launch (below => exit 1); info adds + cpu_floor/cpu_model to the embedded-vs-sidecar field table. `$host` is a + read-only automatic variable in PowerShell - the PS verdict uses `$hlevel` + (caught during testing). Re-wrapped via + `punk::mix::commandset::scriptwrap::multishell punk-runtime -askme 0 -force 1` + from src/scriptapps/bin; wrap is deterministic (re-wrap diff = identical); + the scriptwrap_runtime_cmd_roundtrip_no_drift test PASSES. Host detection + verified (PS level=4 on this Zen 5 box; bash /proc/cpuinfo level=4 in + git-bash); verdict logic verified with a mocked v2 host (x86_64_v4 -> + below/block, znver5 -> unknown, x86_64 -> meets) in both twins. + + Remaining for acceptance (publication-time, not code): rebuild the family + + tools with the floor field in the record, republish in-situ to punkbin (per + Approach 6's alpha dispensation), and run the audit tool against the + republished artifacts to confirm zmm=0 (the acceptance's "flagless build emits + zero instructions above floor"). The [tests]-freshness refresh of the replaced + tomls folds into that republication. A real end-to-end `use`/`run` `below` + block requires a host whose level is below a published artifact's recorded + cpu_model - not demonstrable on this v4 host with baseline-floor artifacts + (every host meets v1); the mechanism is verified by the mocked-host tests. + Behind those: r1-generation disposition and the bin/ punk9* kit rebake. diff --git a/punkproject.toml b/punkproject.toml index 4373ffc3..9b7a2b7f 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,6 +1,6 @@ [project] name = "punkshell" -version = "0.57.0" +version = "0.57.1" license = "BSD-2-Clause" url = "https://www.gitea1.intx.com.au/jn/punkshell" #packager: declared identity for published artifacts (declarative, not proof - diff --git a/scriptlib/developer/cpufloor_audit.tcl b/scriptlib/developer/cpufloor_audit.tcl new file mode 100644 index 00000000..b48be615 --- /dev/null +++ b/scriptlib/developer/cpufloor_audit.tcl @@ -0,0 +1,276 @@ +#cpufloor_audit.tcl - G-172 audit instrument: disassemble a distributed binary +#(or a pre-disassembled .text excerpt) and report instructions above a declared +#CPU instruction-set floor. The acceptance criterion of G-172 ("a flagless build +#of each distributed-artifact recipe emits zero instructions above the declared +#floor") is measurable by THIS tool rather than by a promise about build habits. +# +#The defect class (see goals/G-172-distributed-binary-cpu-floor.md Context): zig +#auto-vectorises ReleaseFast codegen to whatever the build host's CPU happens +#to expose when a recipe resolves cpu_model .determined_by_arch_os by NATIVE +#detection. On a Zen 5 host that is AVX-512 - real EVEX-encoded zmm-operand +#instructions at the front of .text. Tcl has no runtime CPU dispatch, so on a +#non-AVX-512 CPU the first one raises #UD and the process dies with +#STATUS_ILLEGAL_INSTRUCTION (0xC000001D) before writing a byte. The 2026-08-06 +#regression shipped the entire published tclsh9.0.5 family (2756 zmm sites each) +#and the build-path tools (punkzip 1724, punkres 1423) - the bundled fixtures +#are real excerpts of that codegen class. +# +#Detection is by AVX REGISTER WIDTH, the same signal the Context measurements +#used: a `zmm[0-9]+` operand is AVX-512 (x86-64 v4), a `ymm[0-9]+` operand is +#AVX2 (x86-64 v3). Size specifiers (zmmword/ymmword/xmmword) never match - they +#lack the trailing digit. SSE4.2/POPCNT (the v2-only delta over baseline) is +#not detectable by width; baseline and v2 therefore share the same register +#ceiling (no AVX) and audit identically. The regression is AVX-width, so this +#is the honest instrument for it; a future finer-grained floor (per-instruction +#mnemonic classification) is a follow-on, not required by the acceptance. +# +#Plain tclsh, no dependencies. Exit 0 = no instructions above floor (PASS); +#exit 1 = out-of-floor instructions found (FAIL) or a selftest failure. +# +#Usage: +# tclsh cpufloor_audit.tcl ?-floor baseline|v2|v3|v4? +# Disassemble 's .text with a located objdump and audit it. +# llvm-objdump is preferred (Intel syntax), GNU objdump (-M intel) is the +# fallback. No disassembler on PATH is a hard error (install llvm-objdump, +# or pipe a pre-disassembled excerpt in with -from-dis). +# tclsh cpufloor_audit.tcl -from-dis ?-floor ? +# Audit a pre-disassembled .text excerpt (objdump -d output) directly - +# no disassembler needed. The bundled fixtures are this shape. +# tclsh cpufloor_audit.tcl -selftest +# Run the bundled fixtures (scriptlib/developer/cpufloor_fixtures/) through +# every floor and check the verdicts - the regression-as-fixture contract. +# +#Floor defaults to baseline (the project's chosen floor for the x86_64 family - +#see G-172 Approach 2: a Tcl interpreter's codegen delta against v2/v3 is +#negligible against the cost of the default runtime failing to start). + +package require Tcl 8.6 + +proc scriptdir {} { + global argv0 + return [file dirname [file normalize $argv0]] +} + +proc floor_forbidden {floor} { + switch -- $floor { + baseline { return [dict create zmm 1 ymm 1] } + v2 { return [dict create zmm 1 ymm 1] } + v3 { return [dict create zmm 1 ymm 0] } + v4 { return [dict create zmm 0 ymm 0] } + default { error "unknown floor '$floor' (expected baseline, v2, v3 or v4)" } + } +} + +#is an instruction line (addr: hexbytes... mnemonic...)? +proc is_insn_line {line} { + return [regexp {^\s*[0-9a-fA-F]+:\s+([0-9a-fA-F]{2}\s)+} $line] +} + +#extract the mnemonic of an instruction line (first token after the hex-byte run) +proc insn_mnemonic {line} { + if {![regexp {^\s*[0-9a-fA-F]+:\s+(.+)$} $line -> rest]} { return "" } + set rest [regsub {^([0-9a-fA-F]{2}\s+)+} $rest ""] + set rest [regsub {^\s+} $rest ""] + set mnem [lindex [split $rest \t] 0] + return [lindex [split $mnem " "] 0] +} + +#audit a list of disassembly lines against a floor. +#returns dict: total zmm_sites ymm_sites out_of_floor first_rva sample_mnemonics +proc audit_lines {lines floor} { + set forbidden [floor_forbidden $floor] + set fz [dict get $forbidden zmm] + set fy [dict get $forbidden ymm] + set total 0 + set zmm 0 + set ymm 0 + set out 0 + set first_rva "" + set mnemonics {} + foreach line $lines { + if {![is_insn_line $line]} continue + incr total + set line_zmm [expr {[regexp {zmm[0-9]+} $line] ? 1 : 0}] + set line_ymm [expr {[regexp {ymm[0-9]+} $line] ? 1 : 0}] + if {$line_zmm} { incr zmm } + if {$line_ymm} { incr ymm } + set line_out 0 + if {$line_zmm && $fz} { set line_out 1 } + if {$line_ymm && $fy} { set line_out 1 } + if {!$line_out} continue + incr out + if {$first_rva eq ""} { + regexp {^\s*([0-9a-fA-F]+):} $line -> first_rva + } + set mnem [insn_mnemonic $line] + if {$mnem ne "" && $mnem ni $mnemonics && [llength $mnemonics] < 12} { + lappend mnemonics $mnem + } + } + return [dict create total $total zmm_sites $zmm ymm_sites $ymm \ + out_of_floor $out first_rva $first_rva sample_mnemonics $mnemonics] +} + +#locate a disassembler and return a command prefix that disassembles .text in +#Intel syntax. Empty string = none found. +proc find_disassembler {} { + foreach {cmd} { + {llvm-objdump -d --section=.text --x86-asm-syntax=intel} + {objdump -d --section=.text -M intel} + } { + set exe [auto_execok [lindex $cmd 0]] + if {$exe ne ""} { + return [concat [list $exe] [lrange $cmd 1 end]] + } + } + return "" +} + +proc read_lines {path} { + set f [open $path r] + fconfigure $f -translation binary + set data [read $f] + close $f + #normalise CRLF -> LF so the parser sees one line model + return [split [string map [list \r\n \n \r \n] $data] \n] +} + +proc report {path floor res} { + set out [dict get $res out_of_floor] + set zmm [dict get $res zmm_sites] + set ymm [dict get $res ymm_sites] + set total [dict get $res total] + puts "cpufloor_audit: $path" + puts " floor: $floor" + puts " .text insns: $total" + puts " zmm sites (v4): $zmm" + puts " ymm sites (v3): $ymm" + puts " out-of-floor: $out" + if {$out > 0} { + puts " first out-of-floor RVA: [dict get $res first_rva]" + set ms [dict get $res sample_mnemonics] + if {[llength $ms]} { + puts " sample out-of-floor mnemonics: [join $ms {, }]" + } + puts " VERDICT: FAIL - instructions above the declared floor are present" + } else { + puts " VERDICT: PASS - no instructions above the declared floor" + } +} + +proc usage {} { + puts stderr "usage: tclsh cpufloor_audit.tcl ?-floor baseline|v2|v3|v4? " + puts stderr " tclsh cpufloor_audit.tcl -from-dis ?-floor ? " + puts stderr " tclsh cpufloor_audit.tcl -selftest" +} + +proc selftest {} { + set d [file join [scriptdir] cpufloor_fixtures] + set cases { + {avx512.dis.txt baseline 1} + {avx512.dis.txt v2 1} + {avx512.dis.txt v3 1} + {avx512.dis.txt v4 0} + {avx2.dis.txt baseline 1} + {avx2.dis.txt v2 1} + {avx2.dis.txt v3 0} + {avx2.dis.txt v4 0} + {clean.dis.txt baseline 0} + {clean.dis.txt v2 0} + {clean.dis.txt v3 0} + {clean.dis.txt v4 0} + } + set fail 0 + foreach case $cases { + lassign $case fixture floor expect_fail + set p [file join $d $fixture] + if {![file exists $p]} { + puts "SELFTEST FAIL: fixture missing: $p" + incr fail + continue + } + set res [audit_lines [read_lines $p] $floor] + set got [dict get $res out_of_floor] + set got_fail [expr {$got > 0 ? 1 : 0}] + set zmm [dict get $res zmm_sites] + set ymm [dict get $res ymm_sites] + set status OK + if {$got_fail != $expect_fail} { + set status "MISMATCH (expected [expr {$expect_fail ? {FAIL} : {PASS}}])" + incr fail + } + puts [format "SELFTEST %-16s floor=%-9s zmm=%-4d ymm=%-4d out=%-4d %s" \ + $fixture $floor $zmm $ymm $got $status] + } + if {$fail} { + puts "SELFTEST: $fail case(s) failed" + return 1 + } + puts "SELFTEST: all cases pass" + return 0 +} + +#--- argv parsing --- +set floor baseline +set from_dis 0 +set selftest_mode 0 +set positional {} +foreach a $argv { + switch -- $a { + -floor { set expecting_floor 1 } + -from-dis { set from_dis 1 } + -selftest { set selftest_mode 1 } + -help - --help - -h { + usage; exit 0 + } + default { + if {[info exists expecting_floor]} { + set floor $a + unset expecting_floor + } else { + lappend positional $a + } + } + } +} +if {[info exists expecting_floor]} { + usage; exit 1 +} + +if {$selftest_mode} { + exit [selftest] +} + +if {[llength $positional] == 0} { + usage; exit 1 +} +set target [lindex $positional 0] +if {![file exists $target]} { + puts stderr "cpufloor_audit: no such file: $target" + exit 1 +} + +if {$from_dis} { + set lines [read_lines $target] +} else { + set dis [find_disassembler] + if {$dis eq ""} { + puts stderr "cpufloor_audit: no disassembler found on PATH (tried llvm-objdump, objdump)." + puts stderr " install llvm-objdump, or pass a pre-disassembled excerpt with -from-dis." + exit 1 + } + set cmd [concat $dis [list $target]] + if {[catch {exec {*}$cmd 2>@1} disout]} { + #objdump returns nonzero on some warnings but still emits disassembly; + #only treat as fatal if there is no instruction output at all + if {![regexp -inline {[0-9a-fA-F]+:} $disout]} { + puts stderr "cpufloor_audit: disassembler failed: $disout" + exit 1 + } + } + set lines [split [string map [list \r\n \n \r \n] $disout] \n] +} + +set res [audit_lines $lines $floor] +report $target $floor $res +exit [expr {[dict get $res out_of_floor] > 0 ? 1 : 0}] \ No newline at end of file diff --git a/scriptlib/developer/cpufloor_fixtures/avx2.dis.txt b/scriptlib/developer/cpufloor_fixtures/avx2.dis.txt new file mode 100644 index 00000000..392586c4 --- /dev/null +++ b/scriptlib/developer/cpufloor_fixtures/avx2.dis.txt @@ -0,0 +1,32 @@ + +sample.exe: file format coff-x86-64 + +Disassembly of section .text: +140015fc2: c5 fc 10 07 vmovups ymm0, ymmword ptr [rdi] +140016001: c5 fc 11 06 vmovups ymmword ptr [rsi], ymm0 +140016ca6: c5 fc 10 00 vmovups ymm0, ymmword ptr [rax] +140016cad: c5 fc 11 06 vmovups ymmword ptr [rsi], ymm0 +140017842: c4 e2 7d 13 c9 vcvtph2ps ymm1, xmm1 +14001785b: c4 e2 7d 13 db vcvtph2ps ymm3, xmm3 +140017884: c4 e2 7d 13 c0 vcvtph2ps ymm0, xmm0 +140017889: c5 f4 59 c8 vmulps ymm1, ymm1, ymm0 +14001788d: c5 e4 59 c0 vmulps ymm0, ymm3, ymm0 +140017891: c4 e3 7d 1d c9 04 vcvtps2ph xmm1, ymm1, 0x4 +140017897: c4 e3 7d 1d c0 04 vcvtps2ph xmm0, ymm0, 0x4 +1400178c7: c4 e3 7d 1d c9 04 vcvtps2ph xmm1, ymm1, 0x4 +1400178cd: c4 e3 7d 1d c0 04 vcvtps2ph xmm0, ymm0, 0x4 +14001853e: c5 fc 10 01 vmovups ymm0, ymmword ptr [rcx] +140018542: c5 fc 11 00 vmovups ymmword ptr [rax], ymm0 +140018b07: c4 e2 7d 13 ed vcvtph2ps ymm5, xmm5 +140018b0c: c4 e2 7d 13 e4 vcvtph2ps ymm4, xmm4 +140018b11: c5 dc 59 e5 vmulps ymm4, ymm4, ymm5 +140018b15: 62 b3 7d 28 1d e0 04 vcvtps2ph xmm16, ymm4, 0x4 +140018b23: 62 a2 7d 28 13 c8 vcvtph2ps ymm17, xmm16 +140018b2e: 62 f1 74 20 5c ec vsubps ymm5, ymm17, ymm4 +140018b34: 62 b1 5c 28 58 e1 vaddps ymm4, ymm4, ymm17 +140018b3a: c4 e3 7d 1d ed 04 vcvtps2ph xmm5, ymm5, 0x4 +140018b40: c4 e3 7d 1d e4 04 vcvtps2ph xmm4, ymm4, 0x4 +140021500: c4 c1 7c 11 0a vmovups ymmword ptr [r10], ymm1 +1400218d0: c4 c1 7c 11 0e vmovups ymmword ptr [r14], ymm1 +140021d80: c4 c1 7e 7f 06 vmovdqu ymmword ptr [r14], ymm0 +1400221e0: c4 c1 7e 7f 06 vmovdqu ymmword ptr [r14], ymm0 diff --git a/scriptlib/developer/cpufloor_fixtures/avx512.dis.txt b/scriptlib/developer/cpufloor_fixtures/avx512.dis.txt new file mode 100644 index 00000000..d5db792d --- /dev/null +++ b/scriptlib/developer/cpufloor_fixtures/avx512.dis.txt @@ -0,0 +1,70 @@ + +sample.exe: file format coff-x86-64 + +Disassembly of section .text: +140002917: 83 e0 04 and eax, 0x4 +14000291a: 49 29 c1 sub r9, rax +14000291d: 44 89 11 mov dword ptr [rcx], r10d +140002920: 44 8b 14 02 mov r10d, dword ptr [rdx + rax] +140002924: 44 89 14 01 mov dword ptr [rcx + rax], r10d +140002928: 46 8b 14 0a mov r10d, dword ptr [rdx + r9] +14000292c: 42 8b 54 02 fc mov edx, dword ptr [rdx + r8 - 0x4] +140002931: 46 89 14 09 mov dword ptr [rcx + r9], r10d +140002935: 42 89 54 01 fc mov dword ptr [rcx + r8 - 0x4], edx +14000293a: 48 89 c8 mov rax, rcx +14000293d: 5e pop rsi +14000293e: 5d pop rbp +14000293f: c3 ret +140002940: 49 81 f8 ff 00 00 00 cmp r8, 0xff +140002947: 77 3c ja 0x140002985 <.text+0x1985> +140002949: 44 89 c0 mov eax, r8d +14000294c: d1 e8 shr eax +14000294e: 4d 8d 48 c0 lea r9, [r8 - 0x40] +140002952: 62 f1 7c 48 10 02 vmovups zmm0, zmmword ptr [rdx] +140002958: 83 e0 40 and eax, 0x40 +14000295b: 49 29 c1 sub r9, rax +14000295e: 62 f1 7c 48 10 14 02 vmovups zmm2, zmmword ptr [rdx + rax] +140002965: 62 b1 7c 48 10 0c 0a vmovups zmm1, zmmword ptr [rdx + r9] +14000296c: 62 f1 7c 48 11 01 vmovups zmmword ptr [rcx], zmm0 +140002972: 62 f1 7c 48 11 14 01 vmovups zmmword ptr [rcx + rax], zmm2 +140002979: 62 b1 7c 48 11 0c 09 vmovups zmmword ptr [rcx + r9], zmm1 +140002980: e9 12 01 00 00 jmp 0x140002a97 <.text+0x1a97> +140002985: 62 f1 7c 48 10 02 vmovups zmm0, zmmword ptr [rdx] +14000298b: 41 89 d1 mov r9d, edx +14000298e: 41 83 e1 3f and r9d, 0x3f +140002992: 4f 8d 54 01 c0 lea r10, [r9 + r8 - 0x40] +140002997: 49 c1 ea 06 shr r10, 0x6 +14000299b: 44 89 d0 mov eax, r10d +14000299e: 4d 8d 5a ff lea r11, [r10 - 0x1] +1400029a2: 83 e0 07 and eax, 0x7 +1400029a5: 62 f1 7c 48 11 01 vmovups zmmword ptr [rcx], zmm0 +1400029ab: 49 83 fb 07 cmp r11, 0x7 +1400029af: 73 08 jae 0x1400029b9 <.text+0x19b9> +1400029b1: 45 31 db xor r11d, r11d +1400029b4: e9 a9 00 00 00 jmp 0x140002a62 <.text+0x1a62> +1400029b9: be 00 02 00 00 mov esi, 0x200 +1400029be: 49 83 e2 f8 and r10, -0x8 +1400029c2: 45 31 db xor r11d, r11d +1400029c5: 4c 29 ce sub rsi, r9 +1400029c8: 0f 1f 84 00 00 00 00 00 nop dword ptr [rax + rax] +1400029d0: 62 f1 7c 48 28 44 32 f9 vmovaps zmm0, zmmword ptr [rdx + rsi - 0x1c0] +1400029d8: 62 f1 7c 48 28 4c 32 fa vmovaps zmm1, zmmword ptr [rdx + rsi - 0x180] +1400029e0: 62 f1 7c 48 28 54 32 fb vmovaps zmm2, zmmword ptr [rdx + rsi - 0x140] +1400029e8: 49 83 c3 08 add r11, 0x8 +1400029ec: 62 f1 7c 48 11 44 31 f9 vmovups zmmword ptr [rcx + rsi - 0x1c0], zmm0 +1400029f4: 62 f1 7c 48 11 4c 31 fa vmovups zmmword ptr [rcx + rsi - 0x180], zmm1 +1400029fc: 62 f1 7c 48 28 4c 32 fc vmovaps zmm1, zmmword ptr [rdx + rsi - 0x100] +140002a04: 62 f1 7c 48 11 54 31 fb vmovups zmmword ptr [rcx + rsi - 0x140], zmm2 +140002a0c: 62 f1 7c 48 28 54 32 fd vmovaps zmm2, zmmword ptr [rdx + rsi - 0xc0] +140002a14: 62 f1 7c 48 11 4c 31 fc vmovups zmmword ptr [rcx + rsi - 0x100], zmm1 +140002a1c: 62 f1 7c 48 28 4c 32 fe vmovaps zmm1, zmmword ptr [rdx + rsi - 0x80] +140002a24: 62 f1 7c 48 11 54 31 fd vmovups zmmword ptr [rcx + rsi - 0xc0], zmm2 +140002a2c: 62 f1 7c 48 28 54 32 ff vmovaps zmm2, zmmword ptr [rdx + rsi - 0x40] +140002a34: 62 f1 7c 48 11 4c 31 fe vmovups zmmword ptr [rcx + rsi - 0x80], zmm1 +140002a3c: 62 f1 7c 48 28 0c 32 vmovaps zmm1, zmmword ptr [rdx + rsi] +140002a43: 62 f1 7c 48 11 54 31 ff vmovups zmmword ptr [rcx + rsi - 0x40], zmm2 +140002a4b: 62 f1 7c 48 11 0c 31 vmovups zmmword ptr [rcx + rsi], zmm1 +140002a52: 48 81 c6 00 02 00 00 add rsi, 0x200 +140002a59: 4d 39 da cmp r10, r11 +140002a5c: 0f 85 6e ff ff ff jne 0x1400029d0 <.text+0x19d0> +140002a62: 48 85 c0 test rax, rax diff --git a/scriptlib/developer/cpufloor_fixtures/clean.dis.txt b/scriptlib/developer/cpufloor_fixtures/clean.dis.txt new file mode 100644 index 00000000..31fa2f3f --- /dev/null +++ b/scriptlib/developer/cpufloor_fixtures/clean.dis.txt @@ -0,0 +1,70 @@ + +sample.exe: file format coff-x86-64 + +Disassembly of section .text: + +0000000140001000 <.text>: +140001000: 55 push rbp +140001001: 56 push rsi +140001002: 48 89 e5 mov rbp, rsp +140001005: 45 85 c9 test r9d, r9d +140001008: 7e 6e jle 0x140001078 <.text+0x78> +14000100a: 44 89 c8 mov eax, r9d +14000100d: 41 83 f9 01 cmp r9d, 0x1 +140001011: 75 05 jne 0x140001018 <.text+0x18> +140001013: 45 31 c9 xor r9d, r9d +140001016: eb 49 jmp 0x140001061 <.text+0x61> +140001018: 41 89 c2 mov r10d, eax +14000101b: 41 81 e2 fe ff ff 7f and r10d, 0x7ffffffe +140001022: 45 31 c9 xor r9d, r9d +140001025: 66 2e 0f 1f 84 00 00 00 00 00 nop word ptr cs:[rax + rax] +14000102f: 90 nop +140001030: 4f 8b 1c c8 mov r11, qword ptr [r8 + 8*r9] +140001034: 4f 8d 1c 5b lea r11, [r11 + 2*r11] +140001038: 4a 8b 34 ca mov rsi, qword ptr [rdx + 8*r9] +14000103c: 49 01 f3 add r11, rsi +14000103f: 4c 89 1c f1 mov qword ptr [rcx + 8*rsi], r11 +140001043: 4f 8b 5c c8 08 mov r11, qword ptr [r8 + 8*r9 + 0x8] +140001048: 4f 8d 1c 5b lea r11, [r11 + 2*r11] +14000104c: 4a 8b 74 ca 08 mov rsi, qword ptr [rdx + 8*r9 + 0x8] +140001051: 49 01 f3 add r11, rsi +140001054: 4c 89 1c f1 mov qword ptr [rcx + 8*rsi], r11 +140001058: 49 83 c1 02 add r9, 0x2 +14000105c: 4d 39 ca cmp r10, r9 +14000105f: 75 cf jne 0x140001030 <.text+0x30> +140001061: a8 01 test al, 0x1 +140001063: 74 13 je 0x140001078 <.text+0x78> +140001065: 4b 8b 04 c8 mov rax, qword ptr [r8 + 8*r9] +140001069: 48 8d 04 40 lea rax, [rax + 2*rax] +14000106d: 4a 8b 14 ca mov rdx, qword ptr [rdx + 8*r9] +140001071: 48 01 d0 add rax, rdx +140001074: 48 89 04 d1 mov qword ptr [rcx + 8*rdx], rax +140001078: 5e pop rsi +140001079: 5d pop rbp +14000107a: c3 ret +14000107b: 0f 1f 44 00 00 nop dword ptr [rax + rax] +140001080: 55 push rbp +140001081: 41 57 push r15 +140001083: 41 56 push r14 +140001085: 41 54 push r12 +140001087: 56 push rsi +140001088: 57 push rdi +140001089: 53 push rbx +14000108a: 48 83 ec 20 sub rsp, 0x20 +14000108e: 48 8d 6c 24 20 lea rbp, [rsp + 0x20] +140001093: 48 89 d6 mov rsi, rdx +140001096: 48 63 f9 movsxd rdi, ecx +140001099: 48 8d 1c 7f lea rbx, [rdi + 2*rdi] +14000109d: 4c 8d 34 bd 00 00 00 00 lea r14, [4*rdi] +1400010a5: 4c 8d 3c 3f lea r15, [rdi + rdi] +1400010a9: 45 31 e4 xor r12d, r12d +1400010ac: e8 6f 06 00 00 call 0x140001720 <.text+0x720> +1400010b1: 48 8d 05 98 df 02 00 lea rax, [rip + 0x2df98] # 0x14002f050 +1400010b8: 0f 1f 84 00 00 00 00 00 nop dword ptr [rax + rax] +1400010c0: 4a 89 34 e0 mov qword ptr [rax + 8*r12], rsi +1400010c4: 48 8d 0c 37 lea rcx, [rdi + rsi] +1400010c8: 4a 89 4c e0 08 mov qword ptr [rax + 8*r12 + 0x8], rcx +1400010cd: 49 8d 0c 37 lea rcx, [r15 + rsi] +1400010d1: 4a 89 4c e0 10 mov qword ptr [rax + 8*r12 + 0x10], rcx +1400010d6: 48 8d 0c 33 lea rcx, [rbx + rsi] +1400010da: 4a 89 4c e0 18 mov qword ptr [rax + 8*r12 + 0x18], rcx diff --git a/src/buildsuites/suite_tcl90/build905.zig b/src/buildsuites/suite_tcl90/build905.zig index b8606bd7..39c7d1c2 100644 --- a/src/buildsuites/suite_tcl90/build905.zig +++ b/src/buildsuites/suite_tcl90/build905.zig @@ -119,6 +119,14 @@ pub fn build(b: *std.Build) !void { .default_target = .{ .cpu_model = .baseline }, }); + //G-172: the recipe's declared instruction-set floor (x86-64 v1 / baseline). A + //property of the recipe, recorded in every artifact's [provenance] so a build + //or publication cannot silently ship host-tuned code. -Dcpu= raises the + //floor; -Dcpu=native opts into a host-tuned local build (recorded honestly as + //cpu_model in the record, which audit + punk-runtime read back). Keep in sync + //with the default_target cpu_model above. + const cpu_floor = "baseline"; + const optimize = b.standardOptimizeOption(.{}); //set up and display some of the same prefix-dependent vars as Makefile for maintainability @@ -2596,9 +2604,16 @@ pub fn build(b: *std.Build) !void { \\suite = "suite_tcl90" \\toolchain = "zig {s}" \\optimize = "{s}" + \\#cpu_floor: instruction-set floor the recipe DECLARES as its default + \\#target (G-172) - a property of the recipe, not the build host. A + \\#flagless build resolves to this floor. cpu_model is what the build + \\#ACTUALLY resolved to; -Dcpu=native honestly records the host uarch + \\#there, proving the floor is a default, not a restriction. + \\cpu_floor = "{s}" + \\cpu_model = "{s}" \\{s} \\ - , .{ artifact_name, fk.variant, fk.working_name, familyrev, build_id, originurl, packager, project, projecturl, tcl_h_patchlevel, piperepl_block, batteries, builtin.zig_version_string, @tagName(optimize), prov_lines }); + , .{ artifact_name, fk.variant, fk.working_name, familyrev, build_id, originurl, packager, project, projecturl, tcl_h_patchlevel, piperepl_block, batteries, builtin.zig_version_string, @tagName(optimize), cpu_floor, target.result.cpu.model.name, prov_lines }); } var family_tree_base: [family_kits.len]std.Build.LazyPath = undefined; diff --git a/src/buildsuites/suite_tcl90/tools/family_artifacts.tcl b/src/buildsuites/suite_tcl90/tools/family_artifacts.tcl index 7c41fc3d..94ea8b29 100644 --- a/src/buildsuites/suite_tcl90/tools/family_artifacts.tcl +++ b/src/buildsuites/suite_tcl90/tools/family_artifacts.tcl @@ -144,6 +144,17 @@ foreach {variant kitpath} $opt(-kits) { if {$v eq ""} {fail "$artifact embedded record missing v1 field '$k'"} dict set evars $k $v } + #G-172 CPU instruction-set floor: the recipe declares a floor (cpu_floor) and + #the build resolves a cpu model (cpu_model) - both carried in the embedded + #record's [provenance]. The sidecar derives them from the embedded record + #(single source of truth, like origin/packager above) so a rebuilt+republished + #artifact's sidecar agrees with its embedded record by construction. Absent + #on pre-G-172 kits - fail rather than emit a floor-less sidecar. + foreach k {cpu_floor cpu_model} { + set v [record_field $embedded $k] + if {$v eq ""} {fail "$artifact embedded record missing G-172 floor field '$k' (kit predates the G-172 record extension?)"} + dict set evars $k $v + } set m {} lappend m "#punkshell runtime artifact metadata (schema v2, class \"runtime\") - generated by" @@ -198,6 +209,8 @@ foreach {variant kitpath} $opt(-kits) { lappend m "suite = [toml_str $opt(-suite)]" lappend m "toolchain = [toml_str "zig $opt(-zig)"]" lappend m "optimize = [toml_str $opt(-optimize)]" + lappend m "cpu_floor = [toml_str [dict get $evars cpu_floor]]" + lappend m "cpu_model = [toml_str [dict get $evars cpu_model]]" foreach {n uuid} $opt(-provenance) { lappend m "${n}_checkout = [toml_str $uuid]" } diff --git a/src/buildsuites/suite_tcl90/tools/family_check.tcl b/src/buildsuites/suite_tcl90/tools/family_check.tcl index 79eb7eec..b0817da4 100644 --- a/src/buildsuites/suite_tcl90/tools/family_check.tcl +++ b/src/buildsuites/suite_tcl90/tools/family_check.tcl @@ -226,7 +226,7 @@ set embed_script { set rec [read $f] close $f out embed_exists 1 - foreach k {schema variant tcl_patchlevel working_name name revision build_id piperepl origin packager project license build_host_platform} { + foreach k {schema variant tcl_patchlevel working_name name revision build_id piperepl origin packager project license build_host_platform cpu_floor cpu_model} { out embed_$k [rfield $rec $k] } foreach line [split $rec \n] { @@ -250,6 +250,12 @@ if {[dict get $d embed_build_id] eq ""} {fail "embedded record: empty build_id"} foreach k {embed_origin embed_packager embed_project embed_license embed_build_host_platform} { if {![dict exists $d $k] || [dict get $d $k] eq ""} {fail "embedded record: missing v1 field [string range $k 6 end]"} } +#G-172 CPU floor: the embedded [provenance] must carry the declared floor and the +#resolved cpu model (absent on pre-G-172 kits). cpu_floor is the recipe default +#(baseline for the x86_64 family); cpu_model is what the build resolved to. +foreach k {embed_cpu_floor embed_cpu_model} { + if {![dict exists $d $k] || [dict get $d $k] eq ""} {fail "embedded record: missing G-172 floor field [string range $k 6 end]"} +} if {$opt(-expectrev) ne ""} { asserteq $d embed_revision $opt(-expectrev) "embedded record" if {[string match -nocase *.exe $kitname]} { diff --git a/src/scriptapps/bin/punk-runtime.bash b/src/scriptapps/bin/punk-runtime.bash index 5fba9883..60e62cee 100644 --- a/src/scriptapps/bin/punk-runtime.bash +++ b/src/scriptapps/bin/punk-runtime.bash @@ -247,6 +247,76 @@ provenance_class() { } ' } +#G-172 CPU instruction-set floor surfacing and runnability verdict (ps1 +#payload parity). A runtime's toml carries cpu_floor (the recipe's declared +#default floor) and cpu_model (what the build resolved to). host_cpu_level detects +#the local CPU's x86-64 level from /proc/cpuinfo flags (avx512f=v4, avx2=v3, +#sse4_2=v2, sse2=v1); cpu_floor_verdict compares and sets globals so use/run gate +#launching a runtime the local CPU cannot run (a named diagnosis instead of a +#silent STATUS_ILLEGAL_INSTRUCTION 0xC000001D - the 2026-08-06 AVX-512 failure). +#A detection gap (no /proc/cpuinfo) yields level 0 and the verdict never blocks +#on unknown, so a detection failure cannot prevent a launch. +punk_host_cpu_level="" +host_cpu_level() { + [[ -n "$punk_host_cpu_level" ]] && { echo "$punk_host_cpu_level"; return; } + local lvl=0 flags + if [[ -r /proc/cpuinfo ]]; then + flags=$(grep -m1 '^flags' /proc/cpuinfo 2>/dev/null || true) + if printf '%s' "$flags" | grep -qw 'avx512f'; then lvl=4 + elif printf '%s' "$flags" | grep -qw 'avx2'; then lvl=3 + elif printf '%s' "$flags" | grep -qw 'sse4_2'; then lvl=2 + elif printf '%s' "$flags" | grep -qw 'sse2'; then lvl=1 + fi + fi + punk_host_cpu_level=$lvl + echo "$lvl" +} +#G-172 map a recorded cpu_model name to an x86-64 level (1..4), or 0 for a +#specific uarch / unknown. A specific uarch (znver5, skylake...) is NOT a level +#- the codegen audit tool (scriptlib/developer/cpufloor_audit.tcl) is the +#authoritative runnability check for those. +cpu_model_level() { + case "$1" in + x86_64) echo 1;; + baseline) echo 1;; + x86_64_v2) echo 2;; + x86_64_v3) echo 3;; + x86_64_v4) echo 4;; + *) echo 0;; + esac +} +#G-172 floor-vs-host verdict. $1 = path to the runtime's toml. Sets globals: +#floor_verdict (meets|below|unknown|norecord), floor_required, floor_host, +#floor_message, floor_cpu_floor, floor_cpu_model. 'below' => recorded codegen +#exceeds the host - the caller MUST NOT launch (would die with 0xC000001D). +#'unknown' => specific uarch or host undetermined: do not block, surface the audit. +cpu_floor_verdict() { + local toml="$1" + floor_cpu_floor=$(sed -n 's/^[[:space:]]*cpu_floor[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$toml" 2>/dev/null | head -n 1) + floor_cpu_model=$(sed -n 's/^[[:space:]]*cpu_model[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$toml" 2>/dev/null | head -n 1) + floor_host=$(host_cpu_level) + floor_required=0; floor_message="" + if [[ -z "$floor_cpu_model" ]]; then + floor_verdict="norecord"; return + fi + floor_required=$(cpu_model_level "$floor_cpu_model") + if [[ "$floor_required" -eq 0 ]]; then + floor_verdict="unknown" + floor_message="cpu_model='$floor_cpu_model' is a specific uarch (not an x86-64 level); runnability is verified by the codegen audit tool: tclsh scriptlib/developer/cpufloor_audit.tcl (declared floor: $floor_cpu_floor)" + return + fi + if [[ "$floor_host" -eq 0 ]]; then + floor_verdict="unknown" + floor_message="host CPU level undetermined (/proc/cpuinfo unavailable); recorded cpu_model='$floor_cpu_model' (x86-64 v$floor_required), floor='$floor_cpu_floor' - runnability not verified" + return + fi + if [[ "$floor_host" -lt "$floor_required" ]]; then + floor_verdict="below" + floor_message="runtime built for cpu_model='$floor_cpu_model' (x86-64 v$floor_required) but this CPU is x86-64 v$floor_host - launching it would fail with STATUS_ILLEGAL_INSTRUCTION (0xC000001D). Aborting; use '$0 list' for a runtime this CPU can run." + return + fi + floor_verdict="meets" +} #G-103 artifact metadata: a runtime may carry a .toml beside it (emitted #by the buildsuite kit-family-artifacts step / fetched from punkbin). Prints a #short "[variant=... tcl=... rN ...]" summary for list output, "" when absent. @@ -274,6 +344,24 @@ metadata_summary() { #integrity flag: a runtime filed under a platform folder its metadata says it #was not built for (cross-platform fetch/staging misfiling) [[ -n "$target" && "$target" != "$archtail" ]] && parts="$parts !TARGET-MISMATCH:$target" + #G-172 CPU floor surface + runnability annotation. floor= is informational; a + #!CPU-FLOOR:v tag marks a row the local CPU cannot run - gated to the + #LOCAL platform only (a foreign-platform row is cross-build staging). Absent + #on pre-G-172 tomls. (runs in a command-substitution subshell - host_cpu_level + #caches only within this call, which is fine.) + local cfloor cmodel cmlevel hlevel + cfloor=$(sed -n 's/^[[:space:]]*cpu_floor[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$tomlfile" 2>/dev/null | head -n 1) + [[ -n "$cfloor" ]] && parts="$parts floor=$cfloor" + if [[ "$archtail" == "$local_platform" ]]; then + cmodel=$(sed -n 's/^[[:space:]]*cpu_model[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$tomlfile" 2>/dev/null | head -n 1) + if [[ -n "$cmodel" ]]; then + cmlevel=$(cpu_model_level "$cmodel") + if [[ "$cmlevel" -gt 0 ]]; then + hlevel=$(host_cpu_level) + [[ "$hlevel" -gt 0 && "$hlevel" -lt "$cmlevel" ]] && parts="$parts !CPU-FLOOR:v$cmlevel" + fi + fi + fi [[ -n "$parts" ]] && printf '[%s]' "${parts# }" } #operator help (the 'help' action; the no-args case shows show_usage only). @@ -631,6 +719,12 @@ case "$action" in #build-origin class (table-aware read - schema v2) fclass=$(provenance_class < "${archdir}/${tomlname}") [[ -n "$fclass" ]] && echo "provenance: class=$fclass" + #G-172: surface the recorded CPU floor as information. Fetch is UNGATED + #- a cross-platform fetch (-platform

) for another machine is normal + #and a local-CPU verdict would be meaningless there. + ffloor=$(sed -n 's/^[[:space:]]*cpu_floor[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "${archdir}/${tomlname}" 2>/dev/null | head -n 1) + fmodel=$(sed -n 's/^[[:space:]]*cpu_model[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "${archdir}/${tomlname}" 2>/dev/null | head -n 1) + [[ -n "$ffloor" || -n "$fmodel" ]] && echo "cpu floor: floor=$ffloor model=$fmodel" else rm -f "${archdir}/${tomlname}" echo "no artifact metadata toml on server for $runtime (ok for pre-family runtimes)" @@ -874,6 +968,21 @@ case "$action" in done exit 1 fi + #G-172 floor verdict: selecting a runtime the local CPU cannot run is a + #named diagnosis instead of a silent 0xC000001D on the next 'run'. Only + #the LOCAL platform is gated - a foreign -platform folder is cross-build + #staging deployed elsewhere to run. 'below' blocks the selection. + if [[ "$platform" == "$local_platform" ]]; then + utoml="$archdir/$(rootname_of "$1").toml" + if [[ -f "$utoml" ]]; then + cpu_floor_verdict "$utoml" + if [[ "$floor_verdict" == "below" ]]; then + echo "punk-runtime use: $1 - $floor_message" + exit 1 + fi + [[ "$floor_verdict" == "unknown" ]] && echo "punk-runtime use: $1 - $floor_message" + fi + fi #G-103 artifact-tier names (-r, immutable): 'use' MATERIALIZES the #artifact into its WORKING name (name minus -r - what mapvfs and #projects reference), copies its metadata toml alongside, and selects @@ -942,6 +1051,20 @@ case "$action" in fi fi activeruntime_fullpath="$archdir/$activeruntime" + #G-172 floor verdict: launching a runtime the local CPU cannot run is a + #named diagnosis instead of a silent 0xC000001D (#UD before main - the + #2026-08-06 AVX-512 regression failure mode). 'below' aborts before the + #launch; 'unknown' notes the audit tool but does not block (a -Dcpu=native + #local build runs on its own host by definition). + rtoml="$archdir/$(rootname_of "$activeruntime").toml" + if [[ -f "$rtoml" ]]; then + cpu_floor_verdict "$rtoml" + if [[ "$floor_verdict" == "below" ]]; then + echo "punk-runtime run: $activeruntime - $floor_message" + exit 1 + fi + [[ "$floor_verdict" == "unknown" ]] && echo "punk-runtime run: $activeruntime - $floor_message" + fi #echo "using $activeruntime_fullpath" #(the action was already shifted off during the option scan - "$@" is #exactly the runtime's argument list) @@ -1070,7 +1193,7 @@ case "$action" in if [[ -n "$embedded" || -f "$sidecar" ]]; then printf ' %-20s %-36s %s\n' "field" "embedded" "sidecar" disagreements="" - for k in schema name class variant working_name revision target build_id origin packager builder source_url upstream_ref retrieved project project_url license build_host_platform tcl_patchlevel sha1 size built; do + for k in schema name class variant working_name revision target build_id origin packager builder source_url upstream_ref retrieved project project_url license build_host_platform tcl_patchlevel cpu_floor cpu_model sha1 size built; do ev=""; sv="" if [[ -n "$embedded" ]]; then ev=$(printf '%s\n' "$embedded" | sed -n 's/^[[:space:]]*'"$k"'[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' | head -n 1) diff --git a/src/scriptapps/bin/punk-runtime.ps1 b/src/scriptapps/bin/punk-runtime.ps1 index f6d92881..49a4d47f 100644 --- a/src/scriptapps/bin/punk-runtime.ps1 +++ b/src/scriptapps/bin/punk-runtime.ps1 @@ -320,6 +320,22 @@ function Get-PunkRuntimeMetadataSummary { if ($expectedplatform -ne "" -and $fields.ContainsKey('target') -and $fields['target'] -ne $expectedplatform) { $parts += "!TARGET-MISMATCH:$($fields['target'])" } + #G-172 CPU floor surface + runnability annotation. floor= is informational (the + #recipe's declared default); !CPU-FLOOR:v marks a row the local CPU cannot + #run - gated to the LOCAL platform only (a foreign-platform row is cross-build + #staging, not runnable here regardless). Absent on pre-G-172 tomls (no tag). + if ($fields.ContainsKey('cpu_floor') -and $fields['cpu_floor'] -ne "") { + $parts += "floor=$($fields['cpu_floor'])" + } + if ($expectedplatform -eq $script:PunkLocalPlatform -and $fields.ContainsKey('cpu_model') -and $fields['cpu_model'] -ne "") { + $cmLevel = Get-PunkCpuModelLevel $fields['cpu_model'] + if ($cmLevel -gt 0) { + $hLevel = Get-PunkHostCpuLevel + if ($hLevel -gt 0 -and $hLevel -lt $cmLevel) { + $parts += "!CPU-FLOOR:v$cmLevel" + } + } + } if ($parts.Count -eq 0) { return "" } @@ -364,6 +380,99 @@ function Get-PunkRuntimeProvenanceClass { } return "" } +#G-172 CPU instruction-set floor surfacing and runnability verdict. A runtime's +#metadata toml (and embedded record) carry cpu_floor (the recipe's declared default +#floor) and cpu_model (what the build ACTUALLY resolved to). A published artifact +#built at the recipe default records cpu_model="x86_64" (baseline, v1); -Dcpu=native +#honestly records the host uarch. The verdict compares the recorded codegen level +#to the local CPU so selecting/launching a runtime the local CPU cannot run is a +#NAMED diagnosis instead of a silent STATUS_ILLEGAL_INSTRUCTION (0xC000001D) - +#the failure mode of the 2026-08-06 AVX-512 regression (see G-172 Context). Fetch +#stays ungated (a cross-platform fetch for another machine is normal); only the +#local-only actions (use/run) gate, and list annotates rows the host cannot run. +#Host level detection: IsProcessorFeaturePresent (kernel32) with the documented +#PF constants (winnt.h): SSE2=10 (v1 baseline), SSE4.2=38 (v2), AVX2=40 (v3), +#AVX512F=41 (v4). Detected once and cached; a detection failure degrades to 0 +#(unknown) and the verdict never blocks on unknown, so a detection gap cannot +#prevent a launch - only a POSITIVE below-floor verdict blocks. +$script:PunkHostCpuLevel = $null +function Get-PunkHostCpuLevel { + if ($null -ne $script:PunkHostCpuLevel) { return $script:PunkHostCpuLevel } + $code = @' +using System; +using System.Runtime.InteropServices; +public class PunkCpuFeat { + [DllImport("kernel32.dll")] + static extern bool IsProcessorFeaturePresent(uint feature); + public static int Level() { + if (IsProcessorFeaturePresent(41)) return 4; //AVX512F (x86-64 v4) + if (IsProcessorFeaturePresent(40)) return 3; //AVX2 (x86-64 v3) + if (IsProcessorFeaturePresent(38)) return 2; //SSE4.2 (x86-64 v2) + if (IsProcessorFeaturePresent(10)) return 1; //SSE2 (x86-64 v1 baseline) + return 0; + } +} +'@ + try { + Add-Type -TypeDefinition $code -ErrorAction Stop + $script:PunkHostCpuLevel = [PunkCpuFeat]::Level() + } catch { + #P/Invoke unavailable (locked-down host / broken PS): degrade to unknown + $script:PunkHostCpuLevel = 0 + } + return $script:PunkHostCpuLevel +} +#G-172 map a recorded cpu_model name to an x86-64 level (1..4), or 0 for a +#specific uarch / unknown name. The standard zig level models map directly; a +#specific uarch (znver5, skylake, ...) is NOT a level - the codegen audit tool +#(scriptlib/developer/cpufloor_audit.tcl) is the authoritative runnability check +#for those, since punk-runtime cannot map a uarch name to a feature set. +function Get-PunkCpuModelLevel { + param([string] $cpuModel) + switch ($cpuModel) { + "x86_64" { return 1 } + "baseline" { return 1 } + "x86_64_v2" { return 2 } + "x86_64_v3" { return 3 } + "x86_64_v4" { return 4 } + default { return 0 } + } +} +#G-172 floor-vs-host verdict. $fields is the record-field hashtable from +#Get-PunkRuntimeRecordFields on the runtime's toml. Returns a hashtable: +# Verdict 'meets' | 'below' | 'unknown' | 'norecord' +# Required 0..4 (the recorded codegen level; 0 = unknown/specific-uarch) +# Host 0..4 (the local CPU level; 0 = undetermined) +# Message the diagnosis line (empty on meets/norecord) +# CpuFloor / CpuModel as recorded (empty if absent) +#'below' => the recorded codegen exceeds the host: launching would die with +#0xC000001D, so the caller MUST NOT launch. 'unknown' => cannot verdict by name +#(specific uarch or host undetermined): do not block, surface the audit tool. +function Get-PunkCpuFloorVerdict { + param([hashtable] $fields) + $cm = ""; if ($fields.ContainsKey('cpu_model')) { $cm = $fields['cpu_model'] } + $cf = ""; if ($fields.ContainsKey('cpu_floor')) { $cf = $fields['cpu_floor'] } + #NOTE: deliberately NOT named $host - that is a read-only automatic variable + #in PowerShell (the $Host object); reusing it throws VariableNotWritable. + $hlevel = Get-PunkHostCpuLevel + if ($cm -eq "") { + return @{ Verdict='norecord'; Required=0; Host=$hlevel; Message=""; CpuFloor=$cf; CpuModel=$cm } + } + $req = Get-PunkCpuModelLevel $cm + if ($req -eq 0) { + $msg = "cpu_model='$cm' is a specific uarch (not an x86-64 level); runnability is verified by the codegen audit tool: tclsh scriptlib/developer/cpufloor_audit.tcl (declared floor: $cf)" + return @{ Verdict='unknown'; Required=0; Host=$hlevel; Message=$msg; CpuFloor=$cf; CpuModel=$cm } + } + if ($hlevel -eq 0) { + $msg = "host CPU level undetermined (IsProcessorFeaturePresent unavailable); recorded cpu_model='$cm' (x86-64 v$req), floor='$cf' - runnability not verified" + return @{ Verdict='unknown'; Required=$req; Host=0; Message=$msg; CpuFloor=$cf; CpuModel=$cm } + } + if ($hlevel -lt $req) { + $msg = "runtime built for cpu_model='$cm' (x86-64 v$req) but this CPU is x86-64 v$hlevel - launching it would fail with STATUS_ILLEGAL_INSTRUCTION (0xC000001D). Aborting; use 'punk-runtime list' for a runtime this CPU can run." + return @{ Verdict='below'; Required=$req; Host=$hlevel; Message=$msg; CpuFloor=$cf; CpuModel=$cm } + } + return @{ Verdict='meets'; Required=$req; Host=$hlevel; Message=""; CpuFloor=$cf; CpuModel=$cm } +} #G-117 embedded record: zip CENTRAL-DIRECTORY read of the exe-appended archive via #.NET System.IO.Compression (offsets resolve from the end-of-central-directory #record, so the prepended executable data is no obstacle). The target is NEVER @@ -899,6 +1008,16 @@ function psmain { #build-origin class (table-aware read - schema v2) $fclass = Get-PunkRuntimeProvenanceClass ([string[]](Get-Content -Path $tomllocal)) if ($fclass -ne "") { Write-Host "provenance: class=$fclass" } + #G-172: surface the recorded CPU floor as information. Fetch is + #UNGATED - a cross-platform fetch (-platform

) for another + #machine is a normal workflow and a local-CPU verdict would be + #meaningless there, so only the recorded floor/model are printed. + $fflds = Get-PunkRuntimeRecordFields ([string[]](Get-Content -Path $tomllocal)) + $ffloor = ""; if ($fflds.ContainsKey('cpu_floor')) { $ffloor = $fflds['cpu_floor'] } + $fmodel = ""; if ($fflds.ContainsKey('cpu_model')) { $fmodel = $fflds['cpu_model'] } + if ($ffloor -ne "" -or $fmodel -ne "") { + Write-Host "cpu floor: floor=$ffloor model=$fmodel" + } } catch { Write-Host "no artifact metadata toml on server for $runtime (ok for pre-family runtimes)" } @@ -972,6 +1091,25 @@ function psmain { } exit 1 } + #G-172 floor verdict: selecting a runtime the local CPU cannot run is a + #named diagnosis instead of a silent 0xC000001D on the next 'run'. + #Only the LOCAL platform is gated - a foreign -platform folder is + #cross-build staging deployed elsewhere to run. 'below' blocks the + #selection; 'unknown' notes the audit tool but does not block. + if ($arch -eq $script:PunkLocalPlatform) { + $utoml = Join-Path -Path $archfolder -ChildPath ((Get-PunkRuntimeRootName $rtname) + ".toml") + if (Test-Path -Path $utoml -PathType Leaf) { + $uflds = Get-PunkRuntimeRecordFields ([string[]](Get-Content -Path $utoml)) + $uverd = Get-PunkCpuFloorVerdict $uflds + if ($uverd.Verdict -eq 'below') { + Write-Host "punk-runtime use: $rtname - $uverd.Message" + exit 1 + } + if ($uverd.Verdict -eq 'unknown') { + Write-Host "punk-runtime use: $rtname - $uverd.Message" + } + } + } #G-103 artifact-tier names (-r, immutable): 'use' MATERIALIZES the #artifact into its WORKING name (name minus -r - what mapvfs and #projects reference), copies its metadata toml alongside, and selects @@ -1047,6 +1185,24 @@ function psmain { } $active = Join-Path -Path $archfolder -ChildPath $activename write-host "using: $active" + #G-172 floor verdict: launching a runtime the local CPU cannot run + #is a named diagnosis instead of a silent 0xC000001D (#UD before + #main - the failure mode of the 2026-08-06 AVX-512 regression). + #'below' aborts before Start-Process; 'unknown' notes the audit + #tool but does not block (a -Dcpu=native local build runs on + #its own host by definition). + $rtoml = Join-Path -Path $archfolder -ChildPath ((Get-PunkRuntimeRootName $activename) + ".toml") + if (Test-Path -Path $rtoml -PathType Leaf) { + $rflds = Get-PunkRuntimeRecordFields ([string[]](Get-Content -Path $rtoml)) + $rverd = Get-PunkCpuFloorVerdict $rflds + if ($rverd.Verdict -eq 'below') { + Write-Host "punk-runtime run: $activename - $rverd.Message" + exit 1 + } + if ($rverd.Verdict -eq 'unknown') { + Write-Host "punk-runtime run: $activename - $rverd.Message" + } + } if ($PSBoundParameters.opts.Length -gt 0) { $optsType = $PSBoundParameters.opts.GetType() #method can only be called if .opts is not null write-host "type of opts: $($optsType.FullName)" @@ -1399,7 +1555,7 @@ function psmain { if ($havesidecar) { $sfields = Get-PunkRuntimeRecordFields ([string[]](Get-Content -Path $sidecarfile)) } write-host (" {0,-20} {1,-36} {2}" -f "field", "embedded", "sidecar") $disagreements = @() - foreach ($k in @('schema','name','class','variant','working_name','revision','target','build_id','origin','packager','builder','source_url','upstream_ref','retrieved','project','project_url','license','build_host_platform','tcl_patchlevel','sha1','size','built')) { + foreach ($k in @('schema','name','class','variant','working_name','revision','target','build_id','origin','packager','builder','source_url','upstream_ref','retrieved','project','project_url','license','build_host_platform','tcl_patchlevel','cpu_floor','cpu_model','sha1','size','built')) { $ev = ""; $sv = "" if ($efields.ContainsKey($k)) { $ev = $efields[$k] } if ($sfields.ContainsKey($k)) { $sv = $sfields[$k] }