---
generated: true
model: claude-opus-4-7
generated_at: 2026-05-20T16:50:55+00:00
source_commit: bbd84dc65e58598bfa71fafe72764b4076b0869b
watched_paths_digest: 5fd330bb5dc669554017fa64d5ec4a4c30ea4f447b4705c1408c941d0321b8ca
source_doc: docs/generated/design/pipeline/04b-pre-link-passes.md
source_doc_digest: e0d4c50075fe7f2b5839f98c130a2c687a0a415f14824734f4115ff88b074a62
warning: "Auto-generated. May drift from source. Do not edit by hand."
---

# Tests for pipeline/04b-pre-link-passes

## Intent

Tests verify the pre-link IR-pass sequence described in
[`docs/generated/design/pipeline/04b-pre-link-passes.md`](../../../design/pipeline/04b-pre-link-passes.md):
the ordered Phase A / B / C / D passes that run inside
`generateIRForTranslationUnit` in `slang-lower-to-ir.cpp` before
the per-translation-unit IR module is cached and pulled into
`linkAndOptimizeIR` by `linkIR`.

The `-dump-ir` flag emits a `### LOWER-TO-IR:` block whose body
is the IR snapshot at the end of Phase D (after every pre-link
pass has run, and before any post-link pass runs). That block is
the primary observation surface; the tests in this bundle compile
each shader with
`//TEST:SIMPLE(filecheck=CHECK):-target spirv-asm -dump-ir
-o /dev/null -entry main -stage compute` and FileCheck the block.
Two validation-pass diagnostics are exercised with
`//DIAGNOSTIC_TEST:SIMPLE(diag=CHECK):` (recursive-types in D1
and missing-return in D5).

The bundle covers all four phases at the granularity the doc
documents. Several internal-only knobs (`[__unsafeForceInlineEarly]`
user attribute, debug-info insertion, obfuscation) are recorded
under `## Untested claims` rather than tested.

## Functional coverage

| Claim                                                                                                                                                                                                              | Intent     | Anchor                                                                                                                                                                      | Tests                                                                                                                              |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Phase B's lowerDefer (B3) emits the defer body at scope exit so downstream passes see straight-line code; no defer opcode survives.                                                                                | functional | [#lowerdefer](../../../design/pipeline/04b-pre-link-passes.md#lowerdefer)                                                                                                   | [`phase-b-lower-defer-inlines-body-at-scope-exit.slang`](phase-b-lower-defer-inlines-body-at-scope-exit.slang)                     |
| Phase B's lowerDefer emits the defer body at every exit path of the enclosing scope (per the notable-passes section), including the early return.                                                                  | functional | [#lowerdefer](../../../design/pipeline/04b-pre-link-passes.md#lowerdefer)                                                                                                   | [`phase-b-lower-defer-also-fires-on-early-return.slang`](phase-b-lower-defer-also-fires-on-early-return.slang)                     |
| Phase B's lowerErrorHandling (B2) rewrites throwing functions to return Result<T,E>; after Phase B no tryCall opcode survives in the IR.                                                                           | functional | [#lowererrorhandling](../../../design/pipeline/04b-pre-link-passes.md#lowererrorhandling)                                                                                   | [`phase-b-lower-error-handling-removes-try-call.slang`](phase-b-lower-error-handling-removes-try-call.slang)                       |
| Phase B's prelinkIR (B1) pulls in `[unsafeForceInlineEarly]` core-module bodies; the mandatory-early-inlining loop in Phase C then inlines them, so neither the core operator name nor a forwarding call survives. | functional | [#performmandatoryearlyinlining-and-the-surrounding-loop](../../../design/pipeline/04b-pre-link-passes.md#performmandatoryearlyinlining-and-the-surrounding-loop)           | [`phase-c-mandatory-early-inlining-pulls-in-core-helper.slang`](phase-c-mandatory-early-inlining-pulls-in-core-helper.slang)       |
| Phase A row A6 calls lowerFrontEndEntryPointToIR once per entry point so the entry-point IRFunc skeleton exists before the full declaration walk.                                                                  | functional | [#phase-a-ast-walk-and-ir-emission](../../../design/pipeline/04b-pre-link-passes.md#phase-a-ast-walk-and-ir-emission)                                                       | [`phase-a-multiple-entry-points-each-get-irfunc.slang`](phase-a-multiple-entry-points-each-get-irfunc.slang)                       |
| Phase A row A8 emits the kIROp_GlobalHashedStringLiterals aggregate that collects every hashed string literal observed during lowering.                                                                            | functional | [#phase-a-ast-walk-and-ir-emission](../../../design/pipeline/04b-pre-link-passes.md#phase-a-ast-walk-and-ir-emission)                                                       | [`phase-a-global-hashed-string-literals-emitted-for-printf.slang`](phase-a-global-hashed-string-literals-emitted-for-printf.slang) |
| Phase A's lowerFrontEndEntryPointToIR (A6) produces an IRFunc with [entryPoint(...)] and [numThreads(...)] decorations visible in the LOWER-TO-IR dump.                                                            | functional | [#phase-a-ast-walk-and-ir-emission](../../../design/pipeline/04b-pre-link-passes.md#phase-a-ast-walk-and-ir-emission)                                                       | [`phase-a-entry-point-lowered-with-numthreads.slang`](phase-a-entry-point-lowered-with-numthreads.slang)                           |
| Phase C's applySparseConditionalConstantPropagation (C2) folds `7 + 3` to a single constant 10 stored into the buffer.                                                                                             | functional | [#phase-c-mandatory-optimization-passes](../../../design/pipeline/04b-pre-link-passes.md#phase-c-mandatory-optimization-passes)                                             | [`phase-c-sccp-folds-constant-arithmetic.slang`](phase-c-sccp-folds-constant-arithmetic.slang)                                     |
| Phase C's constructSSA (C1) promotes addressable locals to SSA temporaries; trivial `int x = a; x = x + 1;` shows no var/load/store of a local Int.                                                                | functional | [#phase-c-mandatory-optimization-passes](../../../design/pipeline/04b-pre-link-passes.md#phase-c-mandatory-optimization-passes)                                             | [`phase-c-ssa-construction-eliminates-trivial-local.slang`](phase-c-ssa-construction-eliminates-trivial-local.slang)               |
| Phase C's per-function eliminateDeadCode (C5) sweeps unused intra-function instructions; an unread `int dead = a * 999;` is removed before the LOWER-TO-IR dump.                                                   | functional | [#phase-c-mandatory-optimization-passes](../../../design/pipeline/04b-pre-link-passes.md#phase-c-mandatory-optimization-passes)                                             | [`phase-c-dead-code-eliminates-unused-helper.slang`](phase-c-dead-code-eliminates-unused-helper.slang)                             |
| Phase C's simplifyCFG (C3) removes empty/unreachable blocks; an `if (true) ... else ...` collapses to the taken branch with no ifElse opcode.                                                                      | functional | [#phase-c-mandatory-optimization-passes](../../../design/pipeline/04b-pre-link-passes.md#phase-c-mandatory-optimization-passes)                                             | [`phase-c-simplify-cfg-removes-dead-branch.slang`](phase-c-simplify-cfg-removes-dead-branch.slang)                                 |
| Phase D's checkForMissingReturns (D5) warns when a non-void function lacks a return on every control-flow path (target=None, target-agnostic pre-link).                                                            | negative   | [#phase-d-non-essential-validation-stripping-and-finalization](../../../design/pipeline/04b-pre-link-passes.md#phase-d-non-essential-validation-stripping-and-finalization) | [`phase-d-missing-return-warns-on-non-void.slang`](phase-d-missing-return-warns-on-non-void.slang)                                 |
| Phase D's checkForRecursiveTypes (D1) rejects a self-referential struct with the cyclic-reference diagnostic.                                                                                                      | negative   | [#phase-d-non-essential-validation-stripping-and-finalization](../../../design/pipeline/04b-pre-link-passes.md#phase-d-non-essential-validation-stripping-and-finalization) | [`phase-d-check-recursion-rejects-recursive-type.slang`](phase-d-check-recursion-rejects-recursive-type.slang)                     |
| Phase D's final eliminateDeadCode (D12) keeps exports and layouts alive so the post-link linkIR has the mangled-name index built by D15 to resolve against.                                                        | functional | [#phase-d-non-essential-validation-stripping-and-finalization](../../../design/pipeline/04b-pre-link-passes.md#phase-d-non-essential-validation-stripping-and-finalization) | [`phase-d-mangled-name-export-decoration-survives-strip.slang`](phase-d-mangled-name-export-decoration-survives-strip.slang)       |
| Phase D's stripFrontEndOnlyInstructions (D10) removes IRHighLevelDeclDecoration before the LOWER-TO-IR dump.                                                                                                       | functional | [#stripfrontendonlyinstructions](../../../design/pipeline/04b-pre-link-passes.md#stripfrontendonlyinstructions)                                                             | [`phase-d-strip-front-end-only-removes-high-level-decl.slang`](phase-d-strip-front-end-only-removes-high-level-decl.slang)         |
| Phase B's synthesizeBitFieldAccessors (B4) synthesises get/set bodies for bit-field accessors so the early-inlining loop can inline them.                                                                          | functional | [#synthesizebitfieldaccessors](../../../design/pipeline/04b-pre-link-passes.md#synthesizebitfieldaccessors)                                                                 | [`phase-b-synthesize-bit-field-accessors-creates-backing.slang`](phase-b-synthesize-bit-field-accessors-creates-backing.slang)     |

## Untested claims

| Claim                                                                                                                                                                                                 | Reason                | Anchor                                                                                                          | Why untested                                                                                                            |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `[__unsafeForceInlineEarly]` user-code surface — the attribute is internal-only and must not appear in user code; the loop's fixed-point convergence is tested indirectly via C-12.                   | (unclassified)        | (unspecified)                                                                                                   | Reason and explanation to be refined by the next regeneration.                                                          |
| `checkForOperatorShiftOverflow` (D7) — does not reliably diagnose `1 << 40` in the default option set; treat as out-of-scope until a clean surface trigger is documented.                             | (unclassified)        | [#checkforoperatorshiftoverflow](../../../design/pipeline/04b-pre-link-passes.md#checkforoperatorshiftoverflow) | Reason and explanation to be refined by the next regeneration.                                                          |
| `insertDebugValueStore` (B6) — requires `-g` and couples observation to SPIR-V debug instructions outside the pre-link scope.                                                                         | (unclassified)        | [#insertdebugvaluestore](../../../design/pipeline/04b-pre-link-passes.md#insertdebugvaluestore)                 | Reason and explanation to be refined by the next regeneration.                                                          |
| `obfuscateModuleLocs` (D13) runs only when both obfuscation and a source map are requested; the pre-link stripping block sets `stripSourceLocs = false` unconditionally, so locs are never stripped here — with obfuscation but no source map, name hints are stripped while the original locs stay in place. | needs-cli-test        | [#obfuscatemodulelocs](../../../design/pipeline/04b-pre-link-passes.md#obfuscatemodulelocs)                     | Observing the name-hints-stripped-but-locs-preserved state requires driving the obfuscation / source-map options, which the doc names only via the internal `shouldObfuscateCode()` / `shouldHaveSourceMap()` predicates — no CLI spelling this bundle can anchor a test to. |
| The `TraceFunctionCoverage` option-set toggle sets `context->traceFunctionCoverage`; it influences per-decl lowering (function-entry counters) rather than gating a pre-link pass.                    | internal-source-fact  | [#option-set-toggles](../../../design/pipeline/04b-pre-link-passes.md#option-set-toggles)                       | The doc names only the `CompilerOptionName` and the internal context flag — no user-level CLI spelling and no counter inst shape for a `-dump-ir` CHECK to anchor on. See the matching doc-gap row.                     |
| The `TraceBranchCoverage` option-set toggle sets `context->traceBranchCoverage`; it influences per-decl lowering (per-branch-arm counters) rather than gating a pre-link pass.                        | internal-source-fact  | [#option-set-toggles](../../../design/pipeline/04b-pre-link-passes.md#option-set-toggles)                       | Same blocker as `TraceFunctionCoverage`: neither a CLI flag nor the per-branch-arm counter shape is documented, so no observable with-flag vs without-flag difference can be pinned.                                    |
| `prelinkIR` calls `_ensureLinkingInfo()` on every stable input module but not on the mutating `irModule` itself; the linking-info cache is a performance-only acceleration structure and does not change which symbols are imported. | internal-source-fact  | [#prelinkir](../../../design/pipeline/04b-pre-link-passes.md#prelinkir)                                         | Per the doc the cache does not change which symbols are imported, so cache-hit vs rescan has no user-observable output; the symbol pull-in itself is already covered by the mandatory-early-inlining test.              |
| The outer inlining `for(;;)` reuses one `changed` flag: `performMandatoryEarlyInlining` sets it, `peepholeOptimizeGlobalScope` overwrites it, the per-modified-function cluster OR-assigns into it (the trailing `eliminateDeadCode` does not), and the loop breaks once the final value is `false`. | implementation-detail | [#loops-in-the-pipeline](../../../design/pipeline/04b-pre-link-passes.md#loops-in-the-pipeline)                 | Flag-threading inside the fixed-point loop has no test directive that reveals it; only the converged post-Phase-D state is observable in the `### LOWER-TO-IR:` dump.                                                   |
| `prelinkIR`'s pull-in of the `externalSymbolsToPrelink` set populated by upstream lowering — the set itself is not user-visible.                                                                      | (unclassified)        | [#prelinkir](../../../design/pipeline/04b-pre-link-passes.md#prelinkir)                                         | Reason and explanation to be refined by the next regeneration.                                                          |
| `validateIRModuleIfEnabled` (A10, D14) — no-op unless the IR-validation compiler option is set; not exercised by the agentic bundle.                                                                  | (unclassified)        | [#validateirmoduleifenabled](../../../design/pipeline/04b-pre-link-passes.md#validateirmoduleifenabled)         | Reason and explanation to be refined by the next regeneration.                                                          |
| The exact ordering of A1…A10, B1…B6, C1…C7, D1…D15 — the `### LOWER-TO-IR:` dump shows only the post-Phase-D state, not intermediate stages, so per-pass ordering is not observable from this bundle. | implementation-detail | (unspecified)                                                                                                   | Internal compiler choice (pass ordering, hoistability decisions, deduplication) with no test-directive that reveals it. |

## Doc gaps observed

| Anchor                                                                                          | Kind                  | Gap                                                                                                                                                                                                                                                                                                                                                                                                                                     | Suggested addition                                                                                                   |
| ----------------------------------------------------------------------------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| (unspecified)                                                                                   | undocumented-behavior | The doc states that Phase D's per-function eliminateDeadCode (C5) and the final eliminateDeadCode (D12) both run with `keepExportsAlive = true` and `keepLayoutsAlive = true`, but does not name a user-observable consequence of those flags being set rather than unset. The mangled-name-export test (C-16) anchors on the observed result (export decorations survive); a doc sentence calling that out explicitly would be useful. |                                                                                                                      |
| [#lowerexpandtype](../../../design/pipeline/04b-pre-link-passes.md#lowerexpandtype)             | undocumented-behavior | The doc names B5 `lowerExpandType` and explains the IR shape change (pattern nested inside `IRExpand`), but does not provide a Slang surface that produces an `IRExpandType` cleanly enough for the pre-link dump to show the rewrite. The variadic-generics surface this depends on is documented only by reference. A doc gap: name a minimal user-code example that produces `IRExpandType` so the pass's effect is testable.        |                                                                                                                      |
| [#checkautodiffusages](../../../design/pipeline/04b-pre-link-passes.md#checkautodiffusages)     | undocumented-behavior | The doc lists D6 `checkAutoDiffUsages`, D8 `addDecorationsForGenericsSpecializedWithExistentials`, and D9 `checkForMeshOutputReads`, but the diagnostic text and severity each pass emits is not stated.                                                                                                                                                                                                                                | Without verbatim diagnostic text the `DIAGNOSTIC_TEST` anchors are brittle; doc should list at least the error code. |
| [#insertdebugvaluestore](../../../design/pipeline/04b-pre-link-passes.md#insertdebugvaluestore) | undocumented-behavior | The doc names B6 `insertDebugValueStore` gated on `debugInfoLevel >= Standard`, but does not give the surface-observable inst (`DebugValue`) that distinguishes the `-g` vs no-`-g` IR. A claim sentence pointing at the expected post-pass opcode would unblock a focused test.                                                                                                                                                        |                                                                                                                      |
| [#option-set-toggles](../../../design/pipeline/04b-pre-link-passes.md#option-set-toggles)       | missing-surface       | The Option-set toggles table names `TraceCoverage`, `TraceFunctionCoverage`, and `TraceBranchCoverage` via `CompilerOptionName` and internal `context->trace*` flags, saying they influence per-decl lowering (function-entry / per-branch-arm counters), but never names the user-level CLI flag that enables them or the IR shape of the emitted counters.                                                                              | Add the CLI spelling for each trace-coverage toggle and name the counter instruction(s) that per-decl lowering emits, so a `-dump-ir` test can pin the with-flag vs without-flag difference. |
