Repository: KhronosGroup/glslang (C++)

Issue:
Parse error when defining struct fields: unexpected TYPE_NAME, expecting IDENTIFIER
Hi,

This code leads to a parse error:
```glsl
#version 400
struct B
{
    vec3 t;
};
struct K
{
    float A, B; // parse error here
};
void main(){}
```

```
glslang.exe .\test.frag
ERROR: 0:8: '' :  syntax error, unexpected TYPE_NAME, expecting IDENTIFIER
ERROR: 1 compilation errors.  No code generated.
```

To workaround this error, I can either:
- rename the field B (so that it's not the same name as the first struct;
- or split the line into `float A; float B;`
- or switch the order of fields: `float B, A;`

So the parse error happens when I define multiple fields of the same type, and one of them (except the first one) has the same name as a struct define above.

For the context, I'm working on a [shader minifier](https://github.com/laurentlb/shader-minifier), so using single-letter names is useful.


Discussion:
#3996 introduced regressions in other codebases (see #4001). Re-opening.



Patch:
diff --git a/Test/struct_name_as_array_member.frag b/Test/struct_name_as_array_member.frag
new file mode 100644
index 0000000000..10c91abad8
--- /dev/null
+++ b/Test/struct_name_as_array_member.frag
@@ -0,0 +1,14 @@
+#version 400
+
+// Test case for using a struct name as an array member name.
+struct B
+{
+    vec3 t;
+};
+
+struct K
+{
+    float A[2], B[3]; // This should work - struct name B is used as an array member name.
+};
+
+void main(){}
\ No newline at end of file
diff --git a/Test/struct_name_as_struct_member.frag b/Test/struct_name_as_struct_member.frag
new file mode 100644
index 0000000000..cc4ecafb4b
--- /dev/null
+++ b/Test/struct_name_as_struct_member.frag
@@ -0,0 +1,20 @@
+#version 400
+
+// Original bug report from: https://github.com/KhronosGroup/glslang/issues/3931
+// Error was: "syntax error, unexpected TYPE_NAME, expecting IDENTIFIER"
+// when defining a struct field with the same name as a previously defined
+// struct.
+struct B
+{
+    vec3 t;
+};
+
+struct K
+{
+    float A, B; // This should work. The B field is in a different scope
+		// than the B struct.
+};
+
+void main(){
+  int x, B, y, K;
+}
diff --git a/glslang/MachineIndependent/Scan.cpp b/glslang/MachineIndependent/Scan.cpp
index a29a6da25c..a02cd95cae 100644
--- a/glslang/MachineIndependent/Scan.cpp
+++ b/glslang/MachineIndependent/Scan.cpp
@@ -836,12 +836,21 @@ int TScanContext::tokenize(TPpContext* pp, TParserToken& token)
         loc = ppToken.loc;
         parserToken->sType.lex.loc = loc;
         switch (token) {
-        case ';':  afterType = false; afterBuffer = false; return SEMICOLON;
-        case ',':  afterType = false;   return COMMA;
+        case ';':  afterType = false; afterBuffer = false; inDeclaratorList = false; afterDeclarator = false; angleBracketDepth = 0; squareBracketDepth = 0; return SEMICOLON;
+        case ',':
+            // If we just processed a declarator (identifier after a type), this comma
+            // indicates that we're in a declarator list. Note that 'afterDeclarator' is
+            // only set when we are not inside a template parameter list or array expression.
+            if (afterDeclarator) {
+                inDeclaratorList = true;
+            }
+            afterType = false;
+            afterDeclarator = false;
+            return COMMA;
         case ':':                       return COLON;
-        case '=':  afterType = false;   return EQUAL;
-        case '(':  afterType = false;   return LEFT_PAREN;
-        case ')':  afterType = false;   return RIGHT_PAREN;
+        case '=':  afterType = false; inDeclaratorList = false; afterDeclarator = false;   return EQUAL;
+        case '(':  afterType = false; inDeclaratorList = false; afterDeclarator = false;   return LEFT_PAREN;
+        case ')':  afterType = false; inDeclaratorList = false; afterDeclarator = false;   return RIGHT_PAREN;
         case '.':  field = true;        return DOT;
         case '!':                       return BANG;
         case '-':                       return DASH;
@@ -850,16 +859,16 @@ int TScanContext::tokenize(TPpContext* pp, TParserToken& token)
         case '*':                       return STAR;
         case '/':                       return SLASH;
         case '%':                       return PERCENT;
-        case '<':                       return LEFT_ANGLE;
-        case '>':                       return RIGHT_ANGLE;
+        case '<':                       angleBracketDepth++; return LEFT_ANGLE;
+        case '>':                       if (angleBracketDepth > 0) angleBracketDepth--; return RIGHT_ANGLE;
         case '|':                       return VERTICAL_BAR;
         case '^':                       return CARET;
         case '&':                       return AMPERSAND;
         case '?':                       return QUESTION;
-        case '[':                       return LEFT_BRACKET;
-        case ']':                       return RIGHT_BRACKET;
-        case '{':  afterStruct = false; afterBuffer = false; return LEFT_BRACE;
-        case '}':                       return RIGHT_BRACE;
+        case '[':                       squareBracketDepth++; return LEFT_BRACKET;
+        case ']':                       if (squareBracketDepth > 0) squareBracketDepth--; return RIGHT_BRACKET;
+        case '{':  afterStruct = false; afterBuffer = false; inDeclaratorList = false; afterDeclarator = false; angleBracketDepth = 0; squareBracketDepth = 0; return LEFT_BRACE;
+        case '}':  inDeclaratorList = false; afterDeclarator = false; angleBracketDepth = 0; squareBracketDepth = 0; return RIGHT_BRACE;
         case '\\':
             parseContext.error(loc, "illegal use of escape character", "\\", "");
             break;
@@ -1941,13 +1950,26 @@ int TScanContext::identifierOrType()
             if (variable->isUserType() &&
                 // treat redeclaration of forward-declared buffer/uniform reference as an identifier
                 !(variable->getType().isReference() && afterBuffer)) {
-                afterType = true;
 
+                // If we're in a declarator list (like "float a, B;"), treat struct names as IDENTIFIER
+                // to fix GitHub issue #3931
+                if (inDeclaratorList) {
+                    return IDENTIFIER;
+                }
+                
+                afterType = true;
                 return TYPE_NAME;
             }
         }
     }
 
+    // If we see an identifier right after a type, this might be a declarator.
+    // But not in template parameters (inside angle brackets) or array expressions (inside square brackets)
+    if (afterType && angleBracketDepth == 0 && squareBracketDepth == 0) {
+        afterDeclarator = true;
+        afterType = false;
+    }
+
     return IDENTIFIER;
 }
 
diff --git a/glslang/MachineIndependent/ScanContext.h b/glslang/MachineIndependent/ScanContext.h
index 74b2b3c746..dcc074f7bb 100644
--- a/glslang/MachineIndependent/ScanContext.h
+++ b/glslang/MachineIndependent/ScanContext.h
@@ -53,7 +53,7 @@ class TScanContext {
     explicit TScanContext(TParseContextBase& pc) :
         parseContext(pc),
         afterType(false), afterStruct(false),
-        field(false), afterBuffer(false) { }
+        field(false), afterBuffer(false), inDeclaratorList(false), afterDeclarator(false), angleBracketDepth(0), squareBracketDepth(0) { }
     virtual ~TScanContext() { }
 
     static void fillInKeywordMap();
@@ -82,6 +82,10 @@ class TScanContext {
     bool afterStruct;         // true if we've recognized the STRUCT keyword, so can only be looking for an identifier
     bool field;               // true if we're on a field, right after a '.'
     bool afterBuffer;         // true if we've recognized the BUFFER keyword
+    bool inDeclaratorList;    // true if we detected we're in a declarator list like "float a, b;"
+    bool afterDeclarator;     // true if we just saw an identifier after a type (potential declarator)
+    int angleBracketDepth;    // track nesting level of < > to detect template parameters
+    int squareBracketDepth;   // track nesting level of [ ] to detect array expressions
     TSourceLoc loc;
     TParserToken* parserToken;
     TPpToken* ppToken;


Test patch:
diff --git a/gtests/CMakeLists.txt b/gtests/CMakeLists.txt
index cf11815a2f..3c51a49098 100644
--- a/gtests/CMakeLists.txt
+++ b/gtests/CMakeLists.txt
@@ -56,6 +56,7 @@ if(GLSLANG_TESTS)
             ${CMAKE_CURRENT_SOURCE_DIR}/LiveTraverser.FromFile.cpp
             ${CMAKE_CURRENT_SOURCE_DIR}/Pp.FromFile.cpp
             ${CMAKE_CURRENT_SOURCE_DIR}/Spv.FromFile.cpp
+            ${CMAKE_CURRENT_SOURCE_DIR}/StructName.cpp
             ${CMAKE_CURRENT_SOURCE_DIR}/VkRelaxed.FromFile.cpp
             ${CMAKE_CURRENT_SOURCE_DIR}/GlslMapIO.FromFile.cpp)
 
diff --git a/gtests/StructName.cpp b/gtests/StructName.cpp
new file mode 100644
index 0000000000..e57d6867ea
--- /dev/null
+++ b/gtests/StructName.cpp
@@ -0,0 +1,80 @@
+// Copyright (C) 2025 NVIDIA Corporation
+//
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+//
+//    Redistributions of source code must retain the above copyright
+//    notice, this list of conditions and the following disclaimer.
+//
+//    Redistributions in binary form must reproduce the above
+//    copyright notice, this list of conditions and the following
+//    disclaimer in the documentation and/or other materials provided
+//    with the distribution.
+//
+//    Neither the name of 3Dlabs Inc. Ltd. nor the names of its
+//    contributors may be used to endorse or promote products derived
+//    from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+// COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+#include <gtest/gtest.h>
+
+#include "TestFixture.h"
+
+namespace glslangtest {
+namespace {
+
+using StructNameTest = GlslangTest<::testing::Test>;
+
+// Test the original bug report case from issue #3931.
+TEST_F(StructNameTest, OriginalBugReportStructMember)
+{
+    const std::string inputFname = GlobalTestSettings.testRoot + "/struct_name_as_struct_member.frag";
+    std::string input;
+    tryLoadFile(inputFname, "input", &input);
+
+    EShMessages controls = DeriveOptions(Source::GLSL, Semantics::OpenGL, Target::AST);
+    GlslangResult result = compileAndLink("struct_name_as_struct_member.frag", input, "", controls,
+                                          glslang::EShTargetVulkan_1_0, glslang::EShTargetSpv_1_0);
+
+    // Should NOT have the original syntax error that was reported in the GitHub issue.
+    EXPECT_EQ(result.linkingError.find("syntax error, unexpected TYPE_NAME, expecting IDENTIFIER"), std::string::npos);
+
+    // Should compile successfully.
+    EXPECT_EQ(result.shaderResults[0].output.find("compilation errors"), std::string::npos);
+}
+
+// Test struct names used as array member names.
+TEST_F(StructNameTest, StructNameAsArrayMember)
+{
+    const std::string inputFname = GlobalTestSettings.testRoot + "/struct_name_as_array_member.frag";
+    std::string input;
+    tryLoadFile(inputFname, "input", &input);
+
+    EShMessages controls = DeriveOptions(Source::GLSL, Semantics::OpenGL, Target::AST);
+    GlslangResult result = compileAndLink("struct_name_as_array_member.frag", input, "", controls,
+                                          glslang::EShTargetVulkan_1_0, glslang::EShTargetSpv_1_0);
+
+    // Should NOT have the original syntax error that was reported in the GitHub issue.
+    EXPECT_EQ(result.linkingError.find("syntax error, unexpected TYPE_NAME, expecting IDENTIFIER"), std::string::npos);
+
+    // Should compile successfully.
+    EXPECT_EQ(result.shaderResults[0].output.find("compilation errors"), std::string::npos);
+}
+
+} // anonymous namespace
+} // namespace glslangtest
\ No newline at end of file