Repository: shader-slang/slang (2026-05-25T07:21:41Z)

Issue:
this_type leaks into codegen when [Differentiable] function returns an interface type (invalid HLSL, ICE on SPIRV)
## Issue Description

When a `[Differentiable]` function returns an interface (existential) type and a method is dispatched on the returned value, Slang fails to lower `this_type` before emit. On HLSL, this produces invalid output (a forward declaration with `this_type<Tuple_N>` parameter and a mismatched-arity call site). On SPIRV, it produces an internal compiler error.

## Reproducer Code

```hlsl
public struct SurfaceInteraction { public float3 pos; }
public struct MaterialProperties { public float3 albedo; }

public interface IMaterialInstance
{
    MaterialProperties collect_properties(SurfaceInteraction si);
}

struct MyInst : IMaterialInstance
{
    MaterialProperties collect_properties(SurfaceInteraction si)
    {
        MaterialProperties p;
        p.albedo = si.pos;
        return p;
    }
}

[Differentiable]
IMaterialInstance get_inst(SurfaceInteraction si)
{
    return MyInst();
}

RWStructuredBuffer<float3> out_buf;

[shader("compute")]
[numthreads(1,1,1)]
void main()
{
    SurfaceInteraction si;
    si.pos = float3(1, 2, 3);
    out_buf[0] = get_inst(si).collect_properties(si).albedo;
}
```

**Commands:**

```bash
slangc repro.slang -target hlsl  -entry main -stage compute -o repro.hlsl   # exit 0, invalid HLSL
slangc repro.slang -target spirv -entry main -stage compute -o repro.spv    # ICE
```

Removing `[Differentiable]` from `get_inst` makes both bugs disappear — Slang fully specializes the existential and emits clean code.

Reproduced on `slangc -v` → `2026.8.1-20-gf23b358b5`.

## Expected Behavior

Slang either specializes the dispatch away (as it does without `[Differentiable]`), or emits a valid dispatcher with consistent declaration and call-site arity. No `this_type<...>` should reach the backend.

## Actual Behavior

### `-target hlsl`: invalid output (slangc exits 0)

```hlsl
// Forward declaration — never defined, contains internal IR type name:
MaterialProperties_0 IMaterialInstance_collect_properties_0(this_type<Tuple_0 > _0, SurfaceInteraction_0 _1);

// Call site — passes only 1 argument despite the 2-parameter declaration:
MaterialProperties_0 _S1 = IMaterialInstance_collect_properties_0(si_0);
```

dxc rejects this with `unknown type name 'this_type'` and `expected ')'` at the call site.

### `-target spirv`: internal compiler error

```
error[E99997]: Slang compilation aborted due to an exception of N5Slang13InternalErrorE unimplemented: Unhandled global inst in spirv-emit:
let  %1	: _	= this_type(%Tuple)
```

## Test Plan

- [x] Add a regression test under `tests/autodiff/` covering the pattern above, asserting that emitted HLSL contains no `this_type` token and that SPIRV emission succeeds.
- [x] Existing autodiff tests under `tests/autodiff/` continue to pass.



Discussion:
## Internal Triage Report

**Reproduced**: Yes — top-of-tree `18e72e1b6` (`2026.5.2-97-g88ca756ac`).

- `slangc repro.slang -target hlsl -entry main -stage compute -o repro.hlsl` exits 0 but emits invalid HLSL:
  - Forward decl: `MaterialProperties_0 IMaterialInstance_collect_properties_0(this_type<Tuple_0 > _0, SurfaceInteraction_0 _1);`
  - Call site: `MaterialProperties_0 _S1 = IMaterialInstance_collect_properties_0(si_0);` — one argument, two-parameter signature.
- `slangc repro.slang -target spirv -entry main -stage compute -o repro.spv` ICEs:
  ```
  error[E99997]: Slang compilation aborted due to an exception of N5Slang13InternalErrorE unimplemented:
  Unhandled global inst in spirv-emit: let %1 : _ = this_type(%Tuple)
  ```

**Crash site**: SPIR-V emit, `this_type(%Tuple)` reaching the global-inst dispatch unhandled. The AST-level root cause is in `maybeOpenExistential` (`source/slang/slang-check-expr.cpp:233`) which only matches bare `DeclRefType` and silently passes `ModifiedType(IFace, NoDiff)` through.
**Pipeline stage**: semantic checking → lowering → SPIR-V emit.
**Nature**: invalid output (HLSL); ICE (SPIRV).

### Analysis
The `[Differentiable]` decl checker at `source/slang/slang-check-decl.cpp:13970–13990` wraps non-differentiable return types in `ModifiedType(returnType, NoDiffModifierVal)`. Interfaces aren't differentiable by default, so a `[Differentiable]` function returning an interface ends up with `ModifiedType(IFace, NoDiff)` as its return type.

At the call site `get_inst(si).collect_properties(si)`, `maybeOpenExistential` fails its `as<DeclRefType>` check and member lookup proceeds against the unopened interface. No `ExtractExistentialValueExpr` is inserted; lowering emits a direct call to the forward-declared dispatcher, no downstream IR pass repairs it, and `this_type(...)` leaks into emit.

### In flight
**PR #11273** (open, by the reporter) has both a complete root-cause writeup and a fix: unwrap `ModifiedType` before the interface check in `maybeOpenExistential`, and use the unwrapped type when constructing the `ExtractExistentialType` inside `openExistential`. Reviewers should ensure the regression test covers both HLSL invalid-codegen and SPIRV ICE.

### Relevant Code
- `source/slang/slang-check-expr.cpp:233` — `maybeOpenExistential`, the reported root cause.
- `source/slang/slang-check-decl.cpp:13970–13990` — where `[Differentiable]` wraps return types in `ModifiedType(.., NoDiffModifierVal)`.

### Relevant Engineers
- `Sai Praveen Bangaru` — primary autodiff owner; right reviewer for PR #11273.
- `Yong He` — historically reviews semantic-checker changes touching existentials.

### Test Gap
No existing test for `[Differentiable]` + interface return type. PR #11273 should bring one; both HLSL and SPIRV codepaths must be covered.

### Notes
This is in flight via PR #11273. Any duplicate fix work should coordinate with that PR.




Patch:
diff --git a/source/slang/slang-check-expr.cpp b/source/slang/slang-check-expr.cpp
index 935f346a74f..6e4b32d1a8a 100644
--- a/source/slang/slang-check-expr.cpp
+++ b/source/slang/slang-check-expr.cpp
@@ -187,9 +187,15 @@ Expr* SemanticsVisitor::openExistential(Expr* expr, DeclRef<InterfaceDecl> inter
         expr,
         [&](DeclRef<VarDeclBase> varDeclRef)
         {
+            // The interface type stored on the `ExtractExistentialType` should
+            // be the bare interface, not a `ModifiedType` wrapping it (e.g. the
+            // `no_diff` modifier that `[Differentiable]` adds to non-
+            // differentiable return types). Otherwise downstream consumers that
+            // look at the cached "original interface type" by `DeclRefType`
+            // would not recognize it as an interface.
             ExtractExistentialType* openedType = m_astBuilder->getOrCreate<ExtractExistentialType>(
                 varDeclRef,
-                expr->type.type,
+                unwrapModifiedType(expr->type.type),
                 interfaceDeclRef);
 
             ExtractExistentialValueExpr* openedValue =
@@ -232,7 +238,13 @@ Expr* SemanticsVisitor::openExistential(Expr* expr, DeclRef<InterfaceDecl> inter
 ///
 Expr* SemanticsVisitor::maybeOpenExistential(Expr* expr)
 {
-    auto exprType = expr->type.type;
+    // Look through `ModifiedType` (e.g. an interface return wrapped in
+    // `no_diff` by the `[Differentiable]` checker) so the interface check
+    // below succeeds. If we miss this, member lookup never produces an
+    // `ExtractExistentialValueExpr`, the IR never emits the standard
+    // existential-dispatch idiom, and a raw `this_type(...)` leaks all the
+    // way to codegen.
+    auto exprType = unwrapModifiedType(expr->type.type);
 
     if (auto declRefType = as<DeclRefType>(exprType))
     {
@@ -6725,6 +6737,8 @@ Expr* SemanticsExprVisitor::visitIsTypeExpr(IsTypeExpr* expr)
     auto valueType = expr->value->type.type;
     if (auto typeType = as<TypeType>(valueType))
         valueType = typeType->getType();
+    auto unwrappedValueType = unwrapModifiedType(valueType);
+    auto valueInterfaceType = isInterfaceType(unwrappedValueType) ? unwrappedValueType : valueType;
 
     // If value is a subtype of `type`, then this expr is always true.
     auto witness = isSubtype(valueType, expr->typeExpr.type, IsSubTypeOptions::None);
@@ -6755,11 +6769,12 @@ Expr* SemanticsExprVisitor::visitIsTypeExpr(IsTypeExpr* expr)
     // subtype witness for runtime checks.
 
     expr->value = maybeOpenExistential(originalVal);
-    expr->witnessArg = witness ? witness : tryGetSubtypeWitness(expr->typeExpr.type, valueType);
+    expr->witnessArg =
+        witness ? witness : tryGetSubtypeWitness(expr->typeExpr.type, valueInterfaceType);
     if (expr->witnessArg)
     {
         // For now we can only support the scenario where `expr->value` is an interface type.
-        if (!optionalWitness && !isInterfaceType(originalVal->type))
+        if (!optionalWitness && !isInterfaceType(valueInterfaceType))
         {
             getSink()->diagnose(Diagnostics::IsOperatorValueMustBeInterfaceType{.expr = expr});
         }
@@ -6772,8 +6787,8 @@ Expr* SemanticsExprVisitor::visitIsTypeExpr(IsTypeExpr* expr)
     // Note: _isTypeParametric only handles DeclRefType-based types (incl. recursive generic args).
     // Builtin composite types like arrays or vectors with embedded generic params are not checked,
     // but these are unlikely to appear in `is`/`as` expressions in practice.
-    if (!as<ErrorType>(valueType) && !as<ErrorType>(expr->typeExpr.type) &&
-        !isInterfaceType(valueType) && !_isTypeParametric(valueType) &&
+    if (!as<ErrorType>(valueInterfaceType) && !as<ErrorType>(expr->typeExpr.type) &&
+        !isInterfaceType(valueInterfaceType) && !_isTypeParametric(valueInterfaceType) &&
         !_isTypeParametric(expr->typeExpr.type))
     {
         getSink()->diagnose(Diagnostics::IsAsOnUnrelatedConcreteTypes{.expr = expr});
@@ -6796,6 +6811,9 @@ Expr* SemanticsExprVisitor::visitAsTypeExpr(AsTypeExpr* expr)
     }
 
     expr->value = CheckTerm(expr->value);
+    auto valueType = expr->value->type.type;
+    auto unwrappedValueType = unwrapModifiedType(valueType);
+    auto valueInterfaceType = isInterfaceType(unwrappedValueType) ? unwrappedValueType : valueType;
 
     // Reject `expr as OpaqueType` (and structs containing opaque fields) because
     // Optional<T> cannot wrap resource/opaque types.
@@ -6811,7 +6829,7 @@ Expr* SemanticsExprVisitor::visitAsTypeExpr(AsTypeExpr* expr)
     expr->type = optType;
 
     // If value is a subtype of `type`, then this expr is equivalent to a CastToSuperTypeExpr.
-    if (auto witness = tryGetSubtypeWitness(expr->value->type.type, typeExpr.type))
+    if (auto witness = tryGetSubtypeWitness(valueType, typeExpr.type))
     {
         auto castToSuperType = createCastToSuperTypeExpr(typeExpr.type, expr->value, witness);
         auto makeOptional = m_astBuilder->create<MakeOptionalExpr>();
@@ -6825,11 +6843,11 @@ Expr* SemanticsExprVisitor::visitAsTypeExpr(AsTypeExpr* expr)
 
     // If target type is an interface type, we will obtain the witness here for
     // runtime casting.
-    expr->witnessArg = tryGetSubtypeWitness(typeExpr.type, expr->value->type.type);
+    expr->witnessArg = tryGetSubtypeWitness(typeExpr.type, valueInterfaceType);
     if (expr->witnessArg)
     {
         // For now we can only support the scenario where `expr->value` is an interface type.
-        if (!isInterfaceType(expr->value->type.type))
+        if (!isInterfaceType(valueInterfaceType))
         {
             getSink()->diagnose(Diagnostics::IsOperatorValueMustBeInterfaceType{.expr = expr});
         }
@@ -6840,9 +6858,8 @@ Expr* SemanticsExprVisitor::visitAsTypeExpr(AsTypeExpr* expr)
     // If we reach here with no witness and both the value type and target type are concrete
     // (not generic type parameters, associated types, or ThisType), the `as` cast always
     // fails and the user is using `as` incorrectly on unrelated concrete types.
-    auto asValueType = expr->value->type.type;
-    if (!as<ErrorType>(asValueType) && !as<ErrorType>(typeExpr.type) &&
-        !isInterfaceType(asValueType) && !_isTypeParametric(asValueType) &&
+    if (!as<ErrorType>(valueInterfaceType) && !as<ErrorType>(typeExpr.type) &&
+        !isInterfaceType(valueInterfaceType) && !_isTypeParametric(valueInterfaceType) &&
         !_isTypeParametric(typeExpr.type))
     {
         getSink()->diagnose(Diagnostics::IsAsOnUnrelatedConcreteTypes{.expr = expr});


Test patch:
diff --git a/tests/autodiff/differentiable-interface-return.slang b/tests/autodiff/differentiable-interface-return.slang
new file mode 100644
index 00000000000..4b2f7ff6bd7
--- /dev/null
+++ b/tests/autodiff/differentiable-interface-return.slang
@@ -0,0 +1,113 @@
+// Regression test for https://github.com/shader-slang/slang/issues/11272.
+//
+// A `[Differentiable]` function that returns an interface (existential) type
+// wraps the return type with a `no_diff` modifier. The semantic checker's
+// `maybeOpenExistential` must look through that modifier so the existential
+// is opened and the lowered IR contains the standard
+// `extractExistential*` + `lookupWitness` dispatch idiom. Without that,
+// `this_type(...)` leaks all the way to codegen — invalid HLSL on
+// `-target hlsl` and an ICE on `-target spirv`.
+
+//TEST:SIMPLE(filecheck=CHECK_HLSL): -target hlsl -stage compute -entry main
+//TEST:SIMPLE(filecheck=CHECK_SPV): -target spirv-asm -stage compute -entry main
+//TEST:INTERPRET(filecheck=CHECK_INTERP): -entry interpretMain
+
+// The fix should ensure `this_type` (the unresolved IR placeholder) does not
+// escape into the emitted HLSL.
+// CHECK_HLSL-NOT: this_type
+// CHECK_HLSL: void main(
+// CHECK_HLSL-NOT: this_type
+// CHECK_HLSL: return;
+// A trailing negative block after the final positive `CHECK_HLSL` extends
+// the scope to end-of-input, so a future regression emitted after `main`
+// still fails the test.
+// CHECK_HLSL-NOT: this_type
+
+// SPIR-V should compile successfully (no ICE) and emit the dispatch chain
+// fully constant-folded into observable stores. Pinning the composites +
+// stores catches silent miscompilation that an `OpEntryPoint`-only check
+// would miss (e.g. a future regression that produces a clean module but
+// folds the call away to undef).
+// CHECK_SPV: OpEntryPoint
+// CHECK_SPV-DAG: %[[F1:[A-Za-z0-9_]+]] = OpConstant %float 1
+// CHECK_SPV-DAG: %[[F2:[A-Za-z0-9_]+]] = OpConstant %float 2
+// CHECK_SPV-DAG: %[[F3:[A-Za-z0-9_]+]] = OpConstant %float 3
+// CHECK_SPV-DAG: %[[V123:[A-Za-z0-9_]+]] = OpConstantComposite %v3float %[[F1]] %[[F2]] %[[F3]]
+// CHECK_SPV-DAG: %[[V111:[A-Za-z0-9_]+]] = OpConstantComposite %v3float %[[F1]] %[[F1]] %[[F1]]
+// CHECK_SPV: OpStore {{.*}} %[[V123]]
+// CHECK_SPV: OpStore {{.*}} %[[V123]]
+// CHECK_SPV: OpStore {{.*}} %[[V111]]
+
+// CHECK_INTERP: matched=1
+// CHECK_INTERP: cast=1 2 3
+// CHECK_INTERP: member=1 2 3
+
+public struct SurfaceInteraction { public float3 pos; }
+public struct MaterialProperties { public float3 albedo; }
+
+public interface IMaterialInstance
+{
+    MaterialProperties collect_properties(SurfaceInteraction si);
+}
+
+struct MyInst : IMaterialInstance
+{
+    MaterialProperties collect_properties(SurfaceInteraction si)
+    {
+        MaterialProperties p;
+        p.albedo = si.pos;
+        return p;
+    }
+}
+
+[Differentiable]
+IMaterialInstance get_inst(SurfaceInteraction si)
+{
+    return MyInst();
+}
+
+// These helpers exercise the same modified-interface return type through
+// `is`/`as`, not just through member lookup.
+bool is_my_inst(SurfaceInteraction si)
+{
+    return get_inst(si) is MyInst;
+}
+
+float3 cast_my_inst(SurfaceInteraction si)
+{
+    let castResult = get_inst(si) as MyInst;
+    if (castResult.hasValue)
+        return castResult.value.collect_properties(si).albedo;
+    return float3(0);
+}
+
+RWStructuredBuffer<float3> out_buf;
+
+void interpretMain()
+{
+    SurfaceInteraction si;
+    si.pos = float3(1, 2, 3);
+
+    bool matched = is_my_inst(si);
+    float3 castResult = cast_my_inst(si);
+    float3 memberResult = get_inst(si).collect_properties(si).albedo;
+
+    printf("matched=%d\n", (int)matched);
+    printf("cast=%.0f %.0f %.0f\n", castResult.x, castResult.y, castResult.z);
+    printf("member=%.0f %.0f %.0f\n", memberResult.x, memberResult.y, memberResult.z);
+}
+
+[shader("compute")]
+[numthreads(1,1,1)]
+void main()
+{
+    SurfaceInteraction si;
+    si.pos = float3(1, 2, 3);
+    bool matched = is_my_inst(si);
+    float3 castResult = cast_my_inst(si);
+    float3 memberResult = get_inst(si).collect_properties(si).albedo;
+
+    out_buf[0] = memberResult;
+    out_buf[1] = castResult;
+    out_buf[2] = matched ? float3(1, 1, 1) : float3(0);
+}