Env report Part 2: warn on misnamed env vars (unknown + compile-time defines)

The boot report printed the raw env and the resolved values, but it never
told the user when a name was WRONG - which is the failure mode that cost
real time: `TMH_NSTATES` was exported as an env var although it is a
compile-time `{.intdefine.}` (`guns/tm_horizon.nim:102`), so the export was
a silent no-op. Add the two warning paths, both boot-only and stdout:

  * unknown TR_*/GUN_* names are named explicitly. The known set is built
    from the modules' exported env-name constants; the remaining inline
    reads are listed once, and common_libs/tests/test_env_report.nim scans
    the tree and fails if a name read anywhere is missing.
  * any `{.intdefine.}`/`{.strdefine.}`/`{.booldefine.}` symbol present as
    an env var is flagged, with the real runtime equivalent when one exists
    (`TMH_NSTATES` -> `TR_TMHORIZON_NSTATES`) and an explicit "does not
    exist - this knob is compile-time only" when it does not. The map is
    derived by grepping the tree; the same test re-greps and fails on drift.

Warnings are emitted only when the env is dirty, so a correct run keeps the
documented A/B/build shape. TR_ENV_REPORT=0 still suppresses everything.

test_env_report.nim: 25 checks (known set, pure helpers, tree literal scan,
tree define scan). All 15 existing guard tests keep their exact counts.
This commit is contained in:
2026-09-23 08:38:37 +02:00
parent 036979e78e
commit f842ac0f76
2 changed files with 322 additions and 0 deletions
+161
View File
@@ -311,6 +311,166 @@ proc printBuildIdentity() =
discard
echo "[env] build binary=", bin, " size=", size, " mtime=", mtime
# ── Part 2: warnings about env mistakes ──────────────────────────────────────
#
# This is the point of the report. A typo must be LOUD, not a silent no-op.
# Two mistakes are caught:
#
# 1. an unknown TR_*/GUN_* name (e.g. TR_TMHORIZON_NSTATE, missing S);
# 2. a compile-time `{.intdefine.}`/`{.strdefine.}`/`{.booldefine.}` symbol
# exported as an env var (e.g. `TMH_NSTATES=2`), which is NEVER read at
# runtime and therefore does nothing.
#
# The known-name set is assembled from the modules' exported env-name
# constants; the remaining inline reads are listed once here. The guard test
# `common_libs/tests/test_env_report.nim` scans the tree for env reads and for
# intdefine/strdefine/booldefine symbols and fails loudly if a name read
# anywhere (or a new define) is missing from these tables, so neither can
# silently drift.
proc knownEnvNames*(): seq[string] =
## Every TR_*/GUN_* name this project reads at runtime. Order is irrelevant;
## callers put it in a set. Keep in sync with the tree scan in the guard test.
result = @[
# exported constants — the single source of truth (do not re-spell these)
MetricEnvVar, SelectorModeEnvVar, TieBreakEnvVar,
PowerPolicyEnvVar, PowerFarDistEnvVar, PowerFarCapEnvVar, PowerMidCapEnvVar,
PowerRefEnvVar, PowerEnergyHiEnvVar, PowerEnergyLoEnvVar,
PowerEnergyMinEnvVar, PowerEnergyMaxEnvVar, PowerFinishKillEnvVar,
PatternRadOffsetEnvVar, PatternRadScaleEnvVar,
TMH_SHIFT_ENV, TMH_BIG_MULT_ENV, TMH_LOG_ENV, TMH_RESET_ON_TARGET_ENV,
TMH_WINDOW_ENV, TMH_RESET_DROP_ENV, TMH_NSTATES_ENV, TMH_ACCURVE_ENV,
TMH_RETRAIN_EVERY_ENV, TMH_EPOCHS_ENV,
]
# rack names are constructed from the prefix + gun table, not spelled out
for g in RackGunNames:
result.add RackEnvPrefix & g
# inline reads with no exported constant (the guard test scans for these)
for n in [
"GUN_SELECTOR_WINDOW", "GUN_SELECTOR_MINOBS", "GUN_SELECTOR_TIE",
"GUN_SELECTOR_FLOOR", "GUN_SELECTOR_POOL", "GUN_SELECTOR_RANK",
"GUN_SELECTOR_SHRINK", "GUN_SELECTOR_DWELL", "GUN_SELECTOR_MARGIN",
"GUN_SELECTOR_POINT_TIE", "GUN_SELECTOR_SEED",
"GUN_RACK_DISABLE", "GUN_STATS_PATH", "GUN_SHOTLOG_PATH",
"TR_MOVEMENT", "TR_MOVEMENT_LOG", "TR_RECORD_WORLDSTATE",
"TR_RADAR_FORCE_SPIN", "TR_RADAR_SCANLOG", "TR_RADAR_SCAN_LOG_PATH",
"TR_TRACKER_PROBE", "TR_TRACKER_PROBE_PATH", "TR_VBULLET_ADMIT_ONLY",
"TR_POWER_LOG",
"TR_RAM_OPPORTUNITY", "TR_RAM_OPP_DIST", "TR_RAM_OPP_MARGIN",
"TR_RAM_ABORT_DMG", "TR_RAM_PLAN", "TR_RAM_PLAN_DIST",
"TR_RAM_PLAN_MARGIN", "TR_RAM_PLAN_HITRATE", "TR_RAM_LOG",
"TR_TFIL_RANGE_LO", "TR_TFIL_RANGE_HI", "TR_TFIL_RANGE_TEMP",
"TR_TFIL_RANGE_K", "TR_TFIL_CORRIDOR_HEAT", "TR_TFIL_WALL_HOTNESS",
# harness vars (read by the test framework, inherited by the bot, so they
# must NOT be reported as typos)
"TR_SERVER_JAR", "TR_BATTLE_RUNNER", "TR_BATTLE_RUNNER_DIR",
# the report's own switch
EnvReportEnableEnvVar,
]:
result.add n
proc unknownEnvNames*(pairs: openArray[(string, string)]): seq[string] =
## Pure: every TR_*/GUN_* name in `pairs` the bot does not read, sorted.
## Anything else (e.g. PATH, JAVA_HOME) is deliberately ignored.
let known = knownEnvNames().toHashSet()
for (key, _) in pairs:
if (key.startsWith("TR_") or key.startsWith("GUN_")) and key notin known:
result.add key
result.sort()
type
DefineMapping* = object
## A `{.intdefine.}`/`{.strdefine.}`/`{.booldefine.}` symbol and the runtime
## env var (if any) that actually controls the same knob. `suggestedEnv` is
## the env name a user would naturally guess, used only for the
## "does not exist" text when `runtimeEnv` is empty.
defineName*: string
runtimeEnv*: string
suggestedEnv*: string
## Derived by grepping the tree for intdefine/strdefine/booldefine (see the
## guard test, which re-greps and fails if this table drifts):
## tm_horizon.nim:101,102,103,106,108,139 — TMH_* ; only NSTATES has an env
## tm_pattern.nim:50,53,55,57,59,62,65,68,69,85,87,92,94,96 — TM_*
## tsetlin.nim:29,116,118,119,120 — TM_*
## docs/env_reference.md documents the same table.
const CompileTimeDefineMap*: seq[DefineMapping] = @[
# tm_horizon.nim — the ONLY define with a runtime twin (`TR_TMHORIZON_NSTATES`)
DefineMapping(defineName: "TMH_NSTATES", runtimeEnv: "TR_TMHORIZON_NSTATES"),
DefineMapping(defineName: "TMH_NCLAUSES", suggestedEnv: "TR_TMHORIZON_NCLAUSES"),
DefineMapping(defineName: "TMH_S_DEF", suggestedEnv: "TR_TMHORIZON_S_DEF"),
DefineMapping(defineName: "TMH_MIN_OBS", suggestedEnv: "TR_TMHORIZON_MIN_OBS"),
DefineMapping(defineName: "TMH_STALE_MAX", suggestedEnv: "TR_TMHORIZON_STALE_MAX"),
# tm_pattern.nim — no env form at all; do not invent a plausible one
DefineMapping(defineName: "TM_NCLAUSES"),
DefineMapping(defineName: "TM_NSTATES"),
DefineMapping(defineName: "TM_MIN_OBS"),
DefineMapping(defineName: "TM_CLASSES"),
DefineMapping(defineName: "TM_CONF_MARGIN_DEF"),
DefineMapping(defineName: "TM_SHRINK_DEF"),
DefineMapping(defineName: "TM_GF_MODE"),
DefineMapping(defineName: "TM_SOFT_BETA_DEF"),
DefineMapping(defineName: "TM_RADIAL_RANGE_DEF"),
DefineMapping(defineName: "TM_RAD_MARGIN_DEF"),
DefineMapping(defineName: "TM_REV_TURN_DEG_DEF"),
DefineMapping(defineName: "TM_REV_MARGIN_DEF"),
DefineMapping(defineName: "TM_REV_GAIN_DEF"),
# tsetlin.nim — no env form at all
DefineMapping(defineName: "TM_N_CLAUSES"),
DefineMapping(defineName: "TM_N_STATES"),
DefineMapping(defineName: "TM_T_DEF"),
DefineMapping(defineName: "TM_WINDOW_SIZE"),
# TM_S_DEF is shared by tm_pattern.nim AND tsetlin.nim (one -d sets both)
DefineMapping(defineName: "TM_S_DEF"),
]
proc isCompileTimeDefine*(name: string): bool =
## True when `name` is one of our -d: symbols (never a runtime env var).
for m in CompileTimeDefineMap:
if m.defineName == name: return true
proc setCompileTimeDefines*(pairs: openArray[(string, string)]): seq[string] =
## Pure: the -d: symbols present as env vars, sorted.
for (key, _) in pairs:
if isCompileTimeDefine(key): result.add key
result.sort()
proc defineWarningMessage*(defineName, value: string): string =
## The exact warning text for one -d: symbol set as an env var. "" when the
## name is not a known define.
for m in CompileTimeDefineMap:
if m.defineName != defineName: continue
let head = "[env] WARNING: " & defineName & "=" & value &
" is a COMPILE-TIME define (-d:" & defineName & "=" & value &
") and is NOT read at runtime; "
if m.runtimeEnv.len > 0:
return head & "the runtime equivalent is " & m.runtimeEnv & "=" & value & "."
if m.suggestedEnv.len > 0:
return head & m.suggestedEnv &
" does not exist - this knob is compile-time only."
return head & "no runtime env equivalent exists - this knob is " &
"compile-time only."
return ""
proc printMistakeWarnings*() =
## The two warning paths, boot-only. Emits nothing when the env is clean, so
## a correct run keeps the documented A/B/build shape.
var pairs: seq[(string, string)]
for key, val in envPairs():
pairs.add (key, val)
let unknown = unknownEnvNames(pairs)
let defines = setCompileTimeDefines(pairs)
if unknown.len == 0 and defines.len == 0: return
echo "[env] --- warnings: misnamed variables ---"
for key in unknown:
echo "[env] WARNING: ", key,
" is not a recognized TR_*/GUN_* variable; it is IGNORED. " &
"Check the spelling (see docs/env_reference.md)."
for key in defines:
echo defineWarningMessage(key, getEnv(key, ""))
# ── the entry point ──────────────────────────────────────────────────────────
var envReportDone = false
@@ -329,6 +489,7 @@ proc printEnvReport*(ctx: EnvReportContext) =
echo "[env] === ENVIRONMENT (boot report) ==="
printRawEnvironment()
printMistakeWarnings()
printEffectiveValues(ctx)
printBuildIdentity()
echo "[env] === END ENVIRONMENT ==="