Repository: shader-slang/slang (2025-07-18T00:53:16Z)

Issue:
enums cannot be used as array indices without explicit cast
**Issue Description**
Unlike in HLSL, enums cannot be used as indices in arrays without explicit casting. However, the documentation states

> By default, the underlying type of an enumeration type is int. Enumeration types are implicitly convertible to their underlying type.

But this doesn't seem to be the case: 

**Reproducer Code**
```slang
enum Foo { A, B, C };

int arr[100];

int main() {
  return arr[Foo::B];
}
```


**Expected Behavior**
I expect either

a) the code to compile without error, similar to HLSL (preferred)
b) the documentation not stating that enums are implicitly convertable to the underlying type
c) the discrepancy between Slang and HLSL being mentioned in the "migrating from HLSL" sections

**Actual Behavior**
Compiling this with `slangc bug.slang` will result in
```
bug.slang(6): error 30014: index expression must evaluate to int.
  return arr[Foo::B];
                  ^
```

Changing `enum` to `enum Foo : int` doesn't help, only explicit casting does:

```slang
return arr[int(Foo::B)];
```

**Environment:**
 - Slang Version 2025.11-12-gc5295eae2
 - Linux

though I have seen the same issue on slangc 2025.07.01-something (can't remember) on Windows.




Discussion:
I have confirmed that this is a code bug (rather than a documentation bug). It is important to note that the Slang compiler allows implicit conversion of an `enum` to its underlying type in other contexts, such as `return` from a function:

```
enum Fruit : int { Apple, Orange };
int getUnderlying( Fruit f ) { return f; }
```

The bug here is likely related to the way that indexing into the built-in array type is special-cased in the compiler rather than going through the logic for invoking a subscript declaration (where the type conversions would automatically be handled consistently with the rest of the language).

A fix for this bug will need to be careful to *not* change the way the compiler treats indexing into an array for any cases that currently work; it should only ensure that it *also* works for types that can be implicitly converted to an `int`. Most likely that would mean continuing to use the existing logic (which I believe simply checks whether the type of the index expression is one of the built-in integer types) as a first pass, and then only if that check fails, trying to implicitly convert the index expression to an `int`.
FWIW, extending array to support other types in subscript also doesn't work (I guess that's a consequence of array indexing being special cased):

```slang
enum E { A, B, C } Foo;
struct S {}

extension<T, A : IArray<T>> A {
    __subscript(E e)->T { get { return this[0];} }
    __subscript(S s)->T { get { return this[0];} }
    T foo() { return this[0];}
}

extension S {
    __subscript(int x)->int { get { return x; } }
    __subscript(E x)->int { get { return 42; } }
}

S s;

int foo(int[] a) {
  return s[42] + s[Foo::B] + a.foo() + a[s] + a[Foo::B];
}
```
`s[42]`, `s[Foo:B]` and `a.foo()` all work, but `a[s]` and `a[Foo::B]` still fail with error 30014.

On a side note: I noticed that only the modern syntax is accepted when defining subscripts, as in `__subscript(Foo foo) -> int { ... }`  works, while `int __subscript(Foo foo) { ... }` doesn't. Is that intentional or should I make a bug report?

There is a hard-coded check in `SemanticsVisitor::CheckSimpleSubscriptExpr`:

```
if (!isScalarIntegerType(indexExpr->type.type))
```

We should generalize this logic. Instead of doing this `if`, we should replace the entire `if` block with something like:

```
auto& indexExpr = subscriptExpr->indexExprs[0];
auto intTargetType = getMatchingIntType(indexExpr->type.type);
indexExpr = coerce(CoercionSite::Argument, intTargetType, indexExpr, getSink());
```

where `getMatchingIntType` is a new helper function we add to SemanticsVisitor, that is defined as:

```
// This function is used to get the best integer type that matches the given type.
// If `type` is already an integer type, return it as is.
// If `type` is a enum type, return the tag type if it exists.
// Otherwise, return the 32-bit signed integer type.
//
Type* SemanticsVisitor::getMatchingIntType(Type* type)
{
    if (isScalarIntType(type))
        return type;
    Type* resultType = type;
    if (auto enumTypeDecl = isDeclRefTypeOf<EnumDecl>(type))
        if (enumTypeDecl->tagType)
            return getMatchingIntType(enumTypeDecl->tagType);
    return m_astBuilder->getIntType();
}
```



Patch:
diff --git a/source/slang/slang-check-decl.cpp b/source/slang/slang-check-decl.cpp
index 46876131a2..131024839e 100644
--- a/source/slang/slang-check-decl.cpp
+++ b/source/slang/slang-check-decl.cpp
@@ -8479,6 +8479,16 @@ bool SemanticsVisitor::isScalarIntegerType(Type* type)
     return isIntegerBaseType(baseType) || baseType == BaseType::Bool;
 }
 
+Type* SemanticsVisitor::getMatchingIntType(Type* type)
+{
+    if (isScalarIntegerType(type))
+        return type;
+    if (auto enumTypeDecl = isDeclRefTypeOf<EnumDecl>(type))
+        if (enumTypeDecl.getDecl()->tagType)
+            return getMatchingIntType(enumTypeDecl.getDecl()->tagType);
+    return m_astBuilder->getIntType();
+}
+
 bool SemanticsVisitor::isHalfType(Type* type)
 {
     auto basicType = as<BasicExpressionType>(type);
diff --git a/source/slang/slang-check-expr.cpp b/source/slang/slang-check-expr.cpp
index 41c3bd510c..2891c316fa 100644
--- a/source/slang/slang-check-expr.cpp
+++ b/source/slang/slang-check-expr.cpp
@@ -2490,13 +2490,10 @@ Expr* SemanticsVisitor::CheckSimpleSubscriptExpr(IndexExpr* subscriptExpr, Type*
     {
         expr = CheckExpr(expr);
     }
-    auto indexExpr = subscriptExpr->indexExprs[0];
+    auto& indexExpr = subscriptExpr->indexExprs[0];
 
-    if (!isScalarIntegerType(indexExpr->type.type))
-    {
-        getSink()->diagnose(indexExpr, Diagnostics::subscriptIndexNonInteger);
-        return CreateErrorExpr(subscriptExpr);
-    }
+    auto intTargetType = getMatchingIntType(indexExpr->type.type);
+    indexExpr = coerce(CoercionSite::Argument, intTargetType, indexExpr, getSink());
 
     subscriptExpr->type = QualType(elementType);
 
diff --git a/source/slang/slang-check-impl.h b/source/slang/slang-check-impl.h
index 86d0b42fe4..5c6be26654 100644
--- a/source/slang/slang-check-impl.h
+++ b/source/slang/slang-check-impl.h
@@ -2139,6 +2139,12 @@ struct SemanticsVisitor : public SemanticsContext
     /// Is `type` a scalar integer type.
     bool isScalarIntegerType(Type* type);
 
+    // This function is used to get the best integer type that matches the given type.
+    // If `type` is already an integer type, return it as is.
+    // If `type` is a enum type, return the tag type if it exists.
+    // Otherwise, return the 32-bit signed integer type.
+    Type* getMatchingIntType(Type* type);
+
     /// Is `type` a scalar half type.
     bool isHalfType(Type* type);
 


Test patch:
diff --git a/tests/language-feature/enums/enum-array-indexing.slang b/tests/language-feature/enums/enum-array-indexing.slang
new file mode 100644
index 0000000000..c9294ad0c0
--- /dev/null
+++ b/tests/language-feature/enums/enum-array-indexing.slang
@@ -0,0 +1,36 @@
+//TEST(compute):COMPARE_COMPUTE(filecheck-buffer=CHECK): -shaderobj -output-using-type
+//TEST(compute):COMPARE_COMPUTE(filecheck-buffer=CHECK):-cpu -shaderobj -output-using-type
+//TEST(compute, vulkan):COMPARE_COMPUTE(filecheck-buffer=CHECK):-vk -shaderobj -output-using-type
+
+// Test that enums can be used as array indices without explicit casting
+
+enum Fruit { Apple, Orange, Banana };
+
+//TEST_INPUT: set outputBuffer = out ubuffer(data=[0 0 0 0], stride=4)
+RWStructuredBuffer<int> outputBuffer;
+
+[numthreads(1,1,1)]
+void computeMain()
+{
+    int fruits[10];
+    
+    // Initialize arrays with some values
+    for (int i = 0; i < 10; i++)
+    {
+        fruits[i] = i * 10;
+    }
+    
+    // Test basic enum indexing - this should work with our fix
+    int appleCost = fruits[Fruit::Apple];     // Should access fruits[0] = 0
+    int orangeCost = fruits[Fruit::Orange];   // Should access fruits[1] = 10
+    int bananaCost = fruits[Fruit::Banana];   // Should access fruits[2] = 20
+    
+    // CHECK: 0
+    outputBuffer[0] = appleCost;
+    // CHECK: 10
+    outputBuffer[1] = orangeCost;
+    // CHECK: 20
+    outputBuffer[2] = bananaCost;
+    // CHECK: 42
+    outputBuffer[3] = 42; // Just a test value
+}
\ No newline at end of file