Repository: shader-slang/slang (2026-04-16T10:07:28Z)

Issue:
Crash when entry point parameter is a struct containing an interface-typed field
## Issue Description

Compiling an entry point whose `uniform` parameter is a struct that contains an interface-typed field causes a segfault on SPIRV and an internal compiler error (99999) on HLSL. The crash occurs even when type conformances are registered via `-conformance` flags.

## Reproducer Code

```hlsl
[anyValueSize(16)]
interface IFoo
{
    float getValue();
}

struct ImplA : IFoo
{
    float v;
    float getValue() { return v; }
}

struct Params
{
    IFoo foo;
    float scale;
}

RWStructuredBuffer<float> outputBuffer;

[numthreads(1, 1, 1)]
void computeMain(uniform Params p)
{
    outputBuffer[0] = p.foo.getValue() * p.scale;
}
```

**Command:**
```bash
slangc test.slang -target spirv -stage compute -entry computeMain -conformance "ImplA:IFoo=0"
```

## Expected Behavior

The compiler should either successfully compile the code (generating dispatch code for the nested interface field) or produce a clear diagnostic explaining that struct-typed entry point parameters with interface fields are not supported.

## Actual Behavior

- SPIRV: segfault (exit code 139, no error message)
- HLSL: `internal error 99999: unimplemented feature in Slang compiler: unexpected IR opcode during code emit`

Note: a direct interface-typed entry point parameter (`uniform IFoo foo`) works correctly with conformances.

## Test Plan

Enable the disabled test `diagnose-entry-point-interface-in-struct.slang` in #10260.


Discussion:
**[Agent] Root Cause Analysis**

The crash occurs because the typeflow specialization pass (`slang-ir-typeflow-specialize.cpp`) fails to discover witness table information for interface-typed fields nested inside struct parameters loaded from constant buffers.

**How the working case flows** (direct `uniform IFoo foo`):
- `collectEntryPointUniformParams` wraps `foo` into `EntryPointParams { IFoo foo; }`
- `analyzeLoad` sees a load of interface type from a resource pointer → calls `findGlobalTables(IFoo)` → discovers all witness tables
- `extractExistentialWitnessTable` gets valid info → dispatch code generated

**How the crashing case fails** (`uniform Params p` where `Params { IFoo foo; }`):
- `load(get_field_addr(CB, p))` returns type `Params` (struct, not interface) — `analyzeLoad`'s interface fast-path doesn't match
- `get_field(loaded_struct, foo)` → `analyzeFieldExtract` checks `fieldInfo` — empty because no `MakeStruct` ever created this struct (it came from GPU memory)
- `extractExistentialWitnessTable` gets no info → no dispatch code → `lookupWitness` survives to emission → HLSL ICE / SPIRV segfault

**Fix**: PR #10856 — in `analyzeFieldExtract` and `analyzeFieldAddress`, when extracting an interface-typed field with no `fieldInfo`, fall back to enumerating globally available witness tables for the interface (mirroring the existing `analyzeLoad` logic).

Note: SPIRV and WGSL targets hit a separate assertion (`resultType`) in storage-to-logical type legalization after the dispatch code is generated. This is a distinct downstream issue.



Patch:
diff --git a/source/slang/slang-ir-typeflow-specialize.cpp b/source/slang/slang-ir-typeflow-specialize.cpp
index 07ba721ddc..356e1a97a2 100644
--- a/source/slang/slang-ir-typeflow-specialize.cpp
+++ b/source/slang/slang-ir-typeflow-specialize.cpp
@@ -2618,6 +2618,40 @@ struct TypeFlowSpecializationContext
                         (IRType*)this->fieldInfo[structField],
                         as<IRPtrTypeBase>(fieldAddress->getDataType()));
                 }
+
+                // When accessing an interface-typed field and no fieldInfo has been
+                // propagated, fall back to enumerating global witness tables.
+                // See the analogous logic in analyzeLoad for direct interface
+                // loads from resource pointers.
+                if (auto interfaceType = as<IRInterfaceType>(structField->getFieldType()))
+                {
+                    if (!isComInterfaceType(interfaceType))
+                    {
+                        HashSet<IRInst*>& tables = *module->getContainerPool().getHashSet<IRInst>();
+                        collectExistentialTables(interfaceType, tables);
+                        if (tables.getCount() > 0)
+                        {
+                            auto valueInfo = makeTaggedUnionType(as<IRWitnessTableSet>(
+                                builder.getSet(kIROp_WitnessTableSet, tables)));
+                            module->getContainerPool().free(&tables);
+                            return builder.getPtrTypeWithAddressSpace(
+                                (IRType*)valueInfo,
+                                as<IRPtrTypeBase>(fieldAddress->getDataType()));
+                        }
+                        module->getContainerPool().free(&tables);
+                    }
+                }
+                else if (
+                    auto boundInterfaceType = as<IRBoundInterfaceType>(structField->getFieldType()))
+                {
+                    auto valueInfo =
+                        makeTaggedUnionType(cast<IRWitnessTableSet>(builder.getSingletonSet(
+                            kIROp_WitnessTableSet,
+                            boundInterfaceType->getWitnessTable())));
+                    return builder.getPtrTypeWithAddressSpace(
+                        (IRType*)valueInfo,
+                        as<IRPtrTypeBase>(fieldAddress->getDataType()));
+                }
             }
         }
 
@@ -2645,6 +2679,35 @@ struct TypeFlowSpecializationContext
             {
                 return this->fieldInfo[structField];
             }
+
+            // When extracting an interface-typed field from a struct and no fieldInfo
+            // has been propagated (e.g. the struct was loaded from a constant buffer
+            // rather than constructed via MakeStruct), fall back to enumerating all
+            // globally available witness tables for the interface. This mirrors the
+            // logic in analyzeLoad for direct interface loads from resource pointers.
+            if (auto interfaceType = as<IRInterfaceType>(structField->getFieldType()))
+            {
+                if (!isComInterfaceType(interfaceType))
+                {
+                    HashSet<IRInst*>& tables = *module->getContainerPool().getHashSet<IRInst>();
+                    collectExistentialTables(interfaceType, tables);
+                    if (tables.getCount() > 0)
+                    {
+                        auto result = makeTaggedUnionType(
+                            as<IRWitnessTableSet>(builder.getSet(kIROp_WitnessTableSet, tables)));
+                        module->getContainerPool().free(&tables);
+                        return result;
+                    }
+                    module->getContainerPool().free(&tables);
+                }
+            }
+            else if (
+                auto boundInterfaceType = as<IRBoundInterfaceType>(structField->getFieldType()))
+            {
+                return makeTaggedUnionType(cast<IRWitnessTableSet>(builder.getSingletonSet(
+                    kIROp_WitnessTableSet,
+                    boundInterfaceType->getWitnessTable())));
+            }
         }
         return none();
     }


Test patch:
diff --git a/tests/language-feature/dynamic-dispatch/diagnose-entry-point-interface-in-struct.slang b/tests/language-feature/dynamic-dispatch/diagnose-entry-point-interface-in-struct.slang
index 519802ef28..f0f7d4812e 100644
--- a/tests/language-feature/dynamic-dispatch/diagnose-entry-point-interface-in-struct.slang
+++ b/tests/language-feature/dynamic-dispatch/diagnose-entry-point-interface-in-struct.slang
@@ -1,10 +1,15 @@
 // A struct containing an interface-typed field used as an entry point
-// parameter currently crashes (SPIRV) or triggers an internal error (HLSL).
-// This test is disabled until the compiler properly handles existential
-// types nested inside struct-typed entry point parameters.
+// parameter should generate dynamic dispatch code when conformances
+// are registered.
+//
+// SPIRV and WGSL targets hit a separate assertion in storage-to-logical
+// type legalization and are left disabled until that is resolved.
 
-//DISABLED_TEST:SIMPLE(filecheck=CHECK): -target spirv -stage compute -entry computeMain -conformance "ImplA:IFoo=0"
-//DISABLED_TEST:SIMPLE(filecheck=CHECK): -target hlsl -stage compute -entry computeMain -conformance "ImplA:IFoo=0"
+//TEST:SIMPLE(filecheck=HLSL): -target hlsl -stage compute -entry computeMain -conformance "ImplA:IFoo=0" -conformance "ImplB:IFoo=1"
+//TEST:SIMPLE(filecheck=GLSL): -target glsl -stage compute -entry computeMain -conformance "ImplA:IFoo=0" -conformance "ImplB:IFoo=1"
+//DISABLED_TEST:SIMPLE(filecheck=SPIRV): -target spirv-asm -stage compute -entry computeMain -conformance "ImplA:IFoo=0" -conformance "ImplB:IFoo=1"
+//TEST:SIMPLE(filecheck=MTL): -target metal -stage compute -entry computeMain -conformance "ImplA:IFoo=0" -conformance "ImplB:IFoo=1"
+//DISABLED_TEST:SIMPLE(filecheck=WGSL): -target wgsl -stage compute -entry computeMain -conformance "ImplA:IFoo=0" -conformance "ImplB:IFoo=1"
 
 [anyValueSize(16)]
 interface IFoo
@@ -18,6 +23,12 @@ struct ImplA : IFoo
     float getValue() { return v; }
 }
 
+struct ImplB : IFoo
+{
+    float v;
+    float getValue() { return v * 2.0; }
+}
+
 struct Params
 {
     IFoo foo;
@@ -26,9 +37,18 @@ struct Params
 
 RWStructuredBuffer<float> outputBuffer;
 
-// CHECK: computeMain
+void takeRef(inout IFoo f, RWStructuredBuffer<float> buf)
+{
+    buf[1] = f.getValue();
+}
+
+// HLSL: s_dispatch_IFoo{{.*}}getValue
+// HLSL: void computeMain
+// GLSL: void main()
+// MTL: void computeMain
 [numthreads(1, 1, 1)]
 void computeMain(uniform Params p)
 {
     outputBuffer[0] = p.foo.getValue() * p.scale;
+    takeRef(p.foo, outputBuffer);
 }