Skip to content

Enforce CompilerOptionName serialization exhaustiveness (classifyCommandLineOption) - #12780

Draft
nv-slang-bot[bot] wants to merge 1 commit into
masterfrom
fix/issue-12257
Draft

Enforce CompilerOptionName serialization exhaustiveness (classifyCommandLineOption)#12780
nv-slang-bot[bot] wants to merge 1 commit into
masterfrom
fix/issue-12257

Conversation

@nv-slang-bot

@nv-slang-bot nv-slang-bot Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Motivation

CompilerOptionSet::writeCommandLineArgs (source/slang/slang-compiler-options.cpp) reconstructs a
descriptive command line that is embedded in debug info — most visibly the SPIR-V DebugEntryPoint
OpString (call site slang-emit-spirv.cpp). It walks the stored option set and, for each option,
runs a hand-maintained switch (option.key) that formats the option's flag(s).

Since #12243 merged, that switch ends in a blanket

default:
    // Other option kinds are currently omitted.
    break;

Because the project builds with -Wno-switch, adding a new CompilerOptionName produces no
compile-time signal
if no arm handles it — the option is silently dropped from the reconstructed
command line. That is exactly the failure class that produced #12220 (-g level hardcoded, several
options dropped). #12243 fixed the immediate cases; per @pdeayton-nv's review the structural fix
(a single classification + an exhaustiveness guard) was pulled out into this follow-up (#12257).

Proposed solution

Reintroduce classifyCommandLineOption(CompilerOptionName) as the single source of truth for how
each option is treated, and enforce its exhaustiveness with a unit test — the incremental path
@jkwak-work approved (issuecomment-5430974889). Every option is classified as one of:

Class Meaning
Serialize Emitted — a descriptive option that influences the generated artifact.
RepresentedElsewhere Not emitted directly — its choice is normalized into another (serialized) option.
Omit Not emitted — intentionally excluded from the reconstruction: context/I/O paths, tooling, diagnostics, deprecated/sentinel values, or an API-only knob with no CLI spelling to reproduce.
Unclassified No decision recorded — a bug, caught by the exhaustiveness unit test.

writeCommandLineArgs now gates on Serialize at the top of its loop (continue otherwise). The
existing per-option formatting switch is kept unchanged — formatting is not pushed into the
parser registry in this PR (that larger, ABI-adjacent refactor is explicitly deferred, per the
triage decision). The blanket default: is replaced by SLANG_ASSERT_FAILURE, since after the gate
it is only reachable by a Serialize option that lacks a formatting arm — a bug we want to fail
loudly rather than silently drop.

This is behavior-preserving: the 78 Serialize-classified options are exactly the 78 options
the existing formatting switch already had a case for (confirmed by set-diffing the classification
against the switch arms — the two sets are identical), so the reconstructed command line is unchanged
for every current option. The regression test tests/spirv/cmd-arg-debug-info.slang (added with
#12243) still passes, and the new commandLineOptionClassificationIsExhaustive unit test passes.

Change summary

File Change
source/slang/slang-compiler-options.h Add CommandLineOptionClass enum + classifyCommandLineOption declaration; update the writeCommandLineArgs doc comment.
source/slang/slang-command-line-option-class.cpp (new) The classifyCommandLineOption definition — the single source of truth. Its own TU (depends only on the enum) so the unit test can recompile it without exporting a DLL symbol, mirroring slang-repro-validator.cpp.
source/slang/slang-compiler-options.cpp Gate the emit loop on Serialize; replace the blanket default: with SLANG_ASSERT_FAILURE.
tools/slang-unit-test/unit-test-command-line-option-classification.cpp (new) commandLineOptionClassificationIsExhaustive — iterates [0, CountOf) and fails on any Unclassified.
tools/CMakeLists.txt Recompile the new .cpp into slang-unit-test (unexported free function, same pattern as slang-repro-validator.cpp).

Concepts and vocabulary

  • Reconstructed / reproduction command line — the CLI-like string writeCommandLineArgs builds
    from a CompilerOptionSet for embedding in debug info; it describes the compile, it is not
    re-parsed by the compiler in normal flow.
  • RepresentedElsewhere vs Omit — both are not emitted, but for different reasons.
    RepresentedElsewhere options carry a choice that is normalized into a serialized option, so
    emitting the source option too would double-count. Omit options are excluded because they
    describe context/I/O/tooling/diagnostics, or (in a few API-only cases) affect emission but have no
    CLI spelling to reconstruct.
  • -Wno-switch — the project disables the unhandled-enum-case warning, which is why a
    compile-checked exhaustive switch cannot enforce this; the unit test substitutes for it.

Process report

Full classification of all 159 CompilerOptionName values (CountOf = 159; last real value
TraceCoverageBindlessIndex = 158). Re-derived deliberately against the current enum — the reviewer-
approved reference in #12243's history (8efc9c0c3f) predates one added option, so its counts were
not copied. Reconciliation (via comm/uniq) confirms every value in [0, CountOf) is
classified exactly once, with no duplicates and no stale names.

Counts: 78 Serialize, 4 RepresentedElsewhere, 77 Omit (+ the CountOf sentinel,
also Omit).

Serialize (78) — influences the generated artifact; emitted onto the reconstructed command line

MacroDefine, Include, Language, MatrixLayoutColumn, MatrixLayoutRow, ZeroInitialize,
IgnoreCapabilities, RestrictiveCapabilityCheck, Profile, SkipSPIRVValidation,
DisableShortCircuit, MinimumSlangOptimization, DisableNonEssentialValidations,
DisableSourceMap, UnscopedEnum, PreserveParameters, Capability, DefaultImageFormatUnknown,
DisableDynamicDispatch, DisableSpecialization, FloatingPointMode, DebugInformation,
DebugInformationFormat, LineDirectiveMode, Optimization, Obfuscate, VulkanBindShift,
VulkanBindShiftAll, VulkanBindGlobals, VulkanInvertY, VulkanUseDxPositionW,
VulkanUseEntryPointName, VulkanUseGLLayout, VulkanEmitReflection, GLSLForceScalarLayout,
EnableEffectAnnotations, IncompleteLibrary, DownstreamArgs, BindlessSpaceIndex,
SPIRVResourceHeapStride, SPIRVSamplerHeapStride, LanguageVersion, TypeConformance,
EnableExperimentalDynamicDispatch, GenerateWholeProgram, ForceDXLayout, DenormalModeFp16,
DenormalModeFp32, DenormalModeFp64, UseMSVCStyleBitfieldPacking, ForceCLayout,
ExperimentalFeature, EmitSeparateDebug, TraceCoverage, TraceCoverageBinding,
TraceCoverageReservedSpace, TraceFunctionCoverage, TraceBranchCoverage,
TraceCoverageCounterByteWidth, TraceCoverageBoolean, TraceCoverageBindlessIndex,
SPIRVUnifiedDescriptorHeapStride, DebugInfoIncludeSource, DumpIntermediates, EmitSpirvMethod,
EmitCPUMethod, EmbedDownstreamIR, NoMangle, NoHLSLBinding,
NoHLSLPackConstantBufferElements, EnableExperimentalPasses, TrackLiveness, LoopInversion,
LLVMTargetTriple, LLVMCPU, LLVMFeatures, AllowGLSL, PassThrough.

RepresentedElsewhere (4) — normalized into a serialized option; emitting the source too would double-count

EmitSpirvViaGLSL, EmitSpirvDirectly, EmitCPUViaCPP, EmitCPUViaLLVM.

The command-line parser folds -emit-spirv-directly / -emit-spirv-via-glsl into the
EmitSpirvMethod enum, and -emit-cpu-via-cpp / -emit-cpu-via-llvm into EmitCPUMethod
(slang-options.cpp). Those two method keys are Serialize, so the source flags must not also be
emitted. (Note: CompilerOptionSet::load — the C-API entry — additionally normalizes the SPIR-V
pair but not the CPU pair; see the follow-up note below.)

Omit (77 + the CountOf sentinel) — excluded from the reconstruction

DepFile, EntryPointName, Specialize, Help, HelpStyle, ModuleName, Output, Stage,
Target, Version, WarningsAsErrors, DisableWarnings, EnableWarning, DisableWarning,
WarningLevel, DumpWarningDiagnostics, InputFilesRemain, EmitIr, ReportDownstreamTime,
ReportPerfBenchmark, ReportCheckpointIntermediates, SourceEmbedStyle, SourceEmbedName,
SourceEmbedLanguage, SPIRVCoreGrammarJSON, CompilerPath, DefaultDownstreamCompiler,
DumpRepro, DumpReproOnError, ExtractRepro, LoadRepro, LoadReproDirectory,
ReproFallbackDirectory, DumpAst, DumpIntermediatePrefix, DumpIr, DumpIrIds,
PreprocessorOutput, OutputIncludes, ReproFileSystem, REMOVED_SerialIR, SkipCodeGen,
ValidateIr, VerbosePaths, VerifyDebugSerialIr, NoCodeGen, FileSystem, Heterogeneous,
ValidateUniformity, ArchiveType, CompileCoreModule, Doc, IrCompression, LoadCoreModule,
ReferenceModule, SaveCoreModule, SaveCoreModuleBinSource, ParameterBlocksUseRegisterSpaces,
EmitReflectionJSON, CountOfParsableOptions, UseUpToDateBinaryModule, SaveGLSLModuleBinSource,
SkipDownstreamLinking, DumpModule, GetModuleInfo, GetSupportedModuleVersions,
ReportDetailedPerfBenchmark, ValidateIRDetailed, DumpIRBefore, DumpIRAfter,
EnableRichDiagnostics, ReportDynamicDispatchSites, EnableMachineReadableDiagnostics,
DiagnosticColor, CompilerVersion, CoverageManifestOutput, SeparateDebugInfoOutput (+ the
CountOf sentinel).

These are target/stage/entry-point context (contributed by the emit site itself),
input/output/module-identity paths, output-policy sidecar paths, repro tooling, dump/introspection,
diagnostics routing & reporting, downstream toolchain selection/paths, deprecated/removed/sentinel
values, and a few API-only knobs (UseUpToDateBinaryModule, SkipDownstreamLinking) that can affect
emission but have no CLI spelling to reconstruct.

Judgment-call options — flagged for maintainer review

Please flag any you would categorize differently:

  • TraceCoverageBindlessIndex (158)Serialize. The only option added since the reference.
    Its own enum doc states the index "is a compile-time constant and therefore part of the compiled
    artifact"; the existing switch already serializes it alongside TraceCoverageReservedSpace.
  • CoverageManifestOutput (150) and SeparateDebugInfoOutput (156)Omit. These are
    output-policy sidecar paths; buildHash explicitly excludes them from the compiler cache key
    (locked by _testCoverageManifestOutputDoesNotAffectCompilerOptionHash /
    _testSeparateDebugInfoOutputDoesNotAffectCompilerOptionHash), so they do not describe the
    artifact bytes.
  • UseUpToDateBinaryModule (115), SkipDownstreamLinking (120)Omit. API-only, no CLI
    spelling; UseUpToDateBinaryModule is likewise excluded from buildHash.
  • DebugInfoIncludeSource (157)Serialize. Affects emitted SPIR-V debug info and has a CLI
    flag.
  • EmitSeparateDebug (124)Serialize (affects emitted artifacts), while its output path
    SeparateDebugInfoOutput is Omit (policy only) — the pair is deliberately split.

Input-shape / layering check

The change adds no new input shape and reads no data by position: classifyCommandLineOption is a
pure key→class function over the existing CompilerOptionName enum, and writeCommandLineArgs
already iterated the option set by key. The classification is the canonical single source of truth
(the switch's formatting arms and the assert-default consume it, rather than re-deriving the
decision), and the exhaustiveness invariant is asserted where a violation would otherwise be silent
(the unit test, standing in for the -Wno-switch-suppressed compile check).

Follow-ups (out of scope here)

  • The "full refactor" — pushing per-option formatting into m_commandOptions/initCommandOptions
    — remains deferred; this PR keeps the formatting switch.
  • CompilerOptionSet::load normalizes the legacy SPIR-V flags into EmitSpirvMethod but does not
    normalize the CPU flags into EmitCPUMethod for API-provided entries. That asymmetry predates this
    PR and does not affect the classification (both source flags are RepresentedElsewhere and both
    method keys are Serialize); noted for a possible follow-up.

Closes #12257.

🤖 Generated by an automated Slang coworker — may be inaccurate. A human maintainer should verify.

Reintroduce classifyCommandLineOption as the single source of truth for how
writeCommandLineArgs treats each CompilerOptionName, classifying every option as
Serialize, RepresentedElsewhere, or Omit. writeCommandLineArgs now gates on the
Serialize classification instead of a blanket default that silently dropped any
unhandled option (the project builds with -Wno-switch, so a missing case gives no
compile signal -- the exact class that produced #12220).

A new unit test (commandLineOptionClassificationIsExhaustive) iterates [0, CountOf)
and fails on any Unclassified option, so a newly-added option cannot be silently
dropped from the reproduction command line.

The classification lives in its own translation unit (slang-command-line-option-class.cpp,
depending only on the enum) so it can be recompiled into the unit-test module without
exporting an internal free function from the DLL ABI.

Behavior-preserving: the set of Serialize-classified options is exactly the set the
existing formatting switch already handled, so the emitted command line is unchanged
for every current option; the switch's default arm is now an unreachable
SLANG_ASSERT_FAILURE.
@nv-slang-bot nv-slang-bot Bot added the pr: non-breaking PRs without breaking changes label Aug 26, 2026
@jhelferty-nv

Copy link
Copy Markdown
Contributor

Automated notice (PR board sync) — do not reply to this comment.

Auto-assigned @jkwak-work as shepherd for this Bot PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr: non-breaking PRs without breaking changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Systematic CompilerOptionName serialization audit + exhaustiveness enforcement

2 participants