Skip to content

[SPIR-V] Add -fspv-allow-import for Import linkage in lib targets#1

Open
Jasper-Bekkers wants to merge 4 commits into
mainfrom
spirv-allow-import
Open

[SPIR-V] Add -fspv-allow-import for Import linkage in lib targets#1
Jasper-Bekkers wants to merge 4 commits into
mainfrom
spirv-allow-import

Conversation

@Jasper-Bekkers
Copy link
Copy Markdown
Member

@Jasper-Bekkers Jasper-Bekkers commented May 2, 2026

Summary

Three commits:

  1. -fspv-allow-import: new flag (gated to lib_* profiles): calls to undefined external functions compile to a body-less OpFunction prototype decorated with LinkageAttributes ... Import, so the resulting SPIR-V module can be linked against a separately compiled definition (e.g. via spirv-link). Mirrors the existing Export path; the diagnostic at SpirvEmitter.cpp:3235 ("found undefined function") was the only thing previously blocking this. Skips SPIRV-Tools legalize/optimize stages when the flag is set — those passes assume every reachable function has a body and crash on Import prototypes; legalization is meaningful only after linking.

  2. Emit Import prototypes before function definitions: in any non-trivial translation unit, other function definitions get emitted around the prototypes — most pervasively when templates from #included headers are lazily instantiated as the entry point body references them. SPIR-V Logical Layout (sec. 2.4) requires all function declarations to precede any function definitions, so the naive insertion-order emit produced def, def, decl, def, ... and spirv-val rejected it with "Function declarations must appear before function definitions". Two-pass forward visit in SpirvModule::invokeVisitor: declarations first, definitions second, insertion order preserved within each pass.

  3. Suppress OpLine inside Import prototype headers: with -Zi, the emitter normally interleaves OpLine before every instruction. Inside an Import prototype that produces an OpLine between OpFunction and the first OpFunctionParameter — structurally valid SPIR-V, but spirv-val classifies OpLine as belonging to FunctionDefinitions outside the Types section, so the layout pass advances past FunctionDeclarations on the first OpLine and the prototype's own OpFunctionEnd is then flagged as a "declaration after definition" violation. The right fix is upstream in SPIRV-Tools (OpLine in FunctionDeclarations is genuinely valid) but that's a slow review cycle that would also require redirecting the bundled SPIRV-Tools submodule to a fork. Sidestep from the DXC side: add an inImportPrototype flag, set on entry to a body-less function and cleared after OpFunctionEnd, and suppress OpLine emit for any instruction inside the prototype's header except OpFunction itself.

End-to-end linking has been verified against the Traverse spirv-linker crate: Traverse-Research/spirv-linker#4.

Test plan

  • Regression tests: lib.fn.import.hlsl, lib.fn.import.export.hlsl, spv.allow-import.requires-lib.hlsl, lib.fn.export.callables.hlsl, lib.fn.import.with.helper.hlsl (covers the reorder + OpLine suppression with -Zi).
  • Full CodeGenSPIRV lit suite: 1566 pass + 2 expected failures, 0 new regressions.
  • End-to-end: build DXC, compile two HLSL files (one export, one calling externals with -fspv-allow-import), link with spirv-linker, assert linked module has no Linkage capability, no LinkageAttributes decorations, no body-less functions, and previously-imported names resolve to functions with bodies.

🤖 Generated with Claude Code

Calls to undefined external functions in `lib_*` targets now compile to a
body-less OpFunction prototype decorated with `LinkageAttributes ... Import`,
so the module can be linked against a separately compiled definition with
e.g. spirv-link. The Export side already worked; this is the missing
counterpart.

Behavior is opt-in via `-fspv-allow-import`. The flag requires a `lib_*`
target profile and skips the SPIRV-Tools legalize/optimize stages, since
those passes assume every reachable function has a body and would crash on
the Import prototypes — optimization is meaningful only after linking.

Adds three regression tests covering Import emission, mixed Import+Export
in one module, and the non-lib-profile diagnostic. Also adds a test for the
existing pure-Export library shape (multiple callables, no entry point).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`-fspv-allow-import` (added in the previous commit) emits body-less
`OpFunction` prototypes decorated with `LinkageAttributes Import` for
external function references. They're discovered in HLSL source order
via `doFunctionDecl`, but in any non-trivial translation unit other
function definitions get emitted around them — most pervasively when
templates from `#include`d headers are lazily instantiated as the
entry point's body references them.

SPIR-V Logical Layout (sec. 2.4) requires all function declarations
(header-only `OpFunction` ... `OpFunctionEnd`) to precede any function
definitions (with body) in the function section. The old single-pass
emit produced output like `def, def, decl, def, ...` for these mixed
cases, which spirv-val rejects with "Function declarations must
appear before function definitions".

Add `SpirvFunction::isDeclaration()` (true when `basicBlocks.empty()`)
and split the function-emit loop in `SpirvModule::invokeVisitor`'s
forward path into two passes: declarations first, definitions second.
Insertion order within each pass is preserved, so behaviour is
unchanged for translation units that don't carry any Import-linked
prototypes.

Regression test `lib.fn.import.with.helper.hlsl` covers a translation
unit that mixes a body-less Import prototype with a body-having helper
function — fails on the old code, passes on the fix.
DXC's emitter normally emits an OpLine before every instruction when
`-Zi` is set, including the OpFunctionParameter / OpFunctionEnd
instructions inside a body-less Import-linkage prototype.

That's structurally valid SPIR-V (the spec allows OpLine almost
anywhere in the function section), but it trips spirv-val's layout
pass: spirv-val classifies OpLine as belonging to FunctionDefinitions
when current section isn't Types (see SPIRV-Tools'
`InstructionLayoutSection`), so the moment it sees the first OpLine
between an Import prototype's OpFunction and OpFunctionParameter it
advances past FunctionDeclarations. The prototype's own OpFunctionEnd
is then flagged as a "declaration after definition" violation.

Fixing it upstream in SPIRV-Tools would be the right thing — OpLine
inside FunctionDeclarations is genuinely valid and the validator's
classification is too strict — but that's a slow review cycle and
would require redirecting the bundled SPIRV-Tools submodule to a
fork. Sidestep it from the DXC side instead: add an `inImportPrototype`
flag to EmitVisitor, set on entry to a body-less function and cleared
after OpFunctionEnd, and suppress OpLine emit for any instruction
inside the prototype's header except OpFunction itself (the OpLine
*before* OpFunction sits outside the function and is fine — the Types
section accepts OpLine).

Cost: Import prototypes lose source-location debug info on their
parameters and OpFunctionEnd. Acceptable — the prototype is dropped
during link-time import resolution anyway, so the debug info would
have been dead.

Test: `lib.fn.import.with.helper.hlsl` now compiles with `-Zi` and
the FileCheck pattern asserts no OpLine appears between OpFunction
and OpFunctionParameter / OpFunctionEnd.
The previous commit suppressed OpLine inside the Import prototype's
header (between OpFunction and OpFunctionParameter / OpFunctionEnd),
but kept the OpLine that initInstruction emits *immediately before*
OpFunction itself. That OpLine sits between the previous emitted
function and this prototype.

When the previous emit was another Import prototype (i.e. consecutive
declarations under the two-pass forward visitor), spirv-val processes
that between-prototype OpLine while in FunctionDeclarations layout
section. OpLine misclassifies as FunctionDefinitions outside Types, so
the validator advances out of FunctionDeclarations — and the *next*
prototype's OpFunctionEnd then trips the "declarations must appear
before definitions" check.

Drop the OpFunction exception. With it, the entire prototype emit
window — including the OpLine immediately before OpFunction — has no
OpLine. Cost: prototypes lose source-location info on their
OpFunction, OpFunctionParameter, and OpFunctionEnd. Acceptable; the
prototype is replaced during link-time import resolution and its
debug info is dead weight from that point on.

Surfaced when shipping the previous commit's binaries to a real
user — the local test fixture has only one Import prototype, so the
between-prototype OpLine never appeared.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant