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_construction_patterns.frag b/Test/struct_construction_patterns.frag
new file mode 100644
index 0000000000..0aebf3fb69
--- /dev/null
+++ b/Test/struct_construction_patterns.frag
@@ -0,0 +1,77 @@
+#version 400
+
+struct Inner {
+    int id;
+    float value;
+};
+
+struct Outer {
+    int before;
+    uint padding;
+    mat2 matrix;
+    int after;
+};
+
+struct Complex {
+    Inner nested;
+    Inner array[2];
+};
+
+struct Point {
+    float x;
+    float y;
+};
+
+struct Color {
+    vec3 rgb;
+    float alpha;
+};
+
+void store_data(Inner data[4]) {
+    // Function that takes array of structs
+}
+
+void store_outer(Outer value) {
+    // Function that takes single struct
+}
+
+void process_point(Point p) {
+    // Function that takes single struct
+}
+
+void main() {
+    // Pattern 1: Complex array constructors (most common tint failure pattern)
+    Outer data[4] = Outer[4](
+        Outer(0, 0u, mat2(vec2(0.0), vec2(0.0)), 0),
+        Outer(1, 1u, mat2(vec2(1.0), vec2(1.0)), 1),
+        Outer(2, 2u, mat2(vec2(2.0), vec2(2.0)), 2),
+        Outer(3, 3u, mat2(vec2(3.0), vec2(3.0)), 3)
+    );
+    
+    // Pattern 2: Nested struct array constructors
+    Inner inner_array[4] = Inner[4](
+        Inner(0, 0.0),
+        Inner(1, 1.0),
+        Inner(2, 2.0),
+        Inner(3, 3.0)
+    );
+    
+    // Pattern 3: Struct with nested struct constructors
+    Complex complex_data = Complex(
+        Inner(42, 3.14),
+        Inner[2](Inner(1, 1.0), Inner(2, 2.0))
+    );
+    
+    // Pattern 4: Function calls with array constructors
+    store_data(Inner[4](Inner(1, 1.0), Inner(2, 2.0), Inner(3, 3.0), Inner(4, 4.0)));
+    store_outer(Outer(10, 10u, mat2(vec2(5.0), vec2(6.0)), 20));
+    
+    // Pattern 5: Basic struct constructors (from deleted basic_struct_constructor.frag)
+    Point p1 = Point(1.0, 2.0);
+    Color c1 = Color(vec3(1.0, 0.0, 0.0), 1.0);
+    Point p2;
+    p2 = Point(3.0, 4.0);
+    
+    // Pattern 6: Struct constructors in function calls (from deleted struct_constructor_in_function_call.frag)
+    process_point(Point(5.0, 6.0));
+} 
\ No newline at end of file
diff --git a/Test/struct_function_parameters.frag b/Test/struct_function_parameters.frag
new file mode 100644
index 0000000000..37f99791a7
--- /dev/null
+++ b/Test/struct_function_parameters.frag
@@ -0,0 +1,48 @@
+#version 400
+
+struct Vertex {
+    vec3 position;
+    vec3 normal;
+};
+
+struct Light {
+    vec3 position;
+    vec3 color;
+    float intensity;
+};
+
+struct Camera {
+    mat4 viewMatrix;
+    mat4 projMatrix;
+};
+
+// Test various positions of struct parameters - these should be TYPE_NAME
+void render(Vertex v, Light l, Camera c) {
+    // Function with struct parameters at start
+}
+
+void process(int id, Vertex v, float time) {
+    // Function with struct parameter in middle
+}
+
+void compute(float delta, int count, Light l, Camera c, bool enabled) {
+    // Function with multiple struct parameters mixed with other types
+}
+
+void single(Vertex v) {
+    // Function with single struct parameter
+}
+
+void multiple(Vertex v1, Vertex v2, Light l1, Light l2) {
+    // Function with multiple parameters of same struct type
+}
+
+void main() {
+    Vertex v = Vertex(vec3(0.0), vec3(1.0, 0.0, 0.0));
+    Light l = Light(vec3(10.0), vec3(1.0), 1.0);
+    Camera c = Camera(mat4(1.0), mat4(1.0));
+    
+    render(v, l, c);
+    process(1, v, 0.5);
+    compute(0.016, 100, l, c, true);
+} 
\ No newline at end of file
diff --git a/Test/struct_member_declarator_lists.frag b/Test/struct_member_declarator_lists.frag
new file mode 100644
index 0000000000..dc80c063a9
--- /dev/null
+++ b/Test/struct_member_declarator_lists.frag
@@ -0,0 +1,49 @@
+#version 400
+
+struct Vector {
+    float x;
+    float y;
+    float z;
+};
+
+struct Color {
+    float r;
+    float g;
+    float b;
+};
+
+struct Material {
+    float shininess;
+};
+
+struct B {
+    vec3 t;
+};
+
+struct K {
+    // Original #3931 issue: struct names used as member names in declarator lists
+    // These should be IDENTIFIER tokens, not TYPE_NAME
+    float Vector, Color;           // Two struct names as member names
+    int Material, shininess;       // Mix of struct name and regular name
+    vec2 a, position, b;          // Regular names in declarator list
+    mat3 ambient, diffuse, spec;  // Regular names in declarator list
+    
+    // Original bug report pattern
+    float A, B;                   // B is a struct name used as member name
+    
+    // Array member pattern
+    float A_array[2], B_array[3]; // B is a struct name used as array member name
+};
+
+void main() {
+    K k;
+    // Access the members that have struct names
+    k.Vector = 1.0;
+    k.Color = 2.0;
+    k.Material = 3;
+    k.B = 4.0;
+    k.B_array[0] = 5.0;
+    
+    // Test variable declarations with struct names
+    int x, B, y, K;
+} 
\ 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..18e89c62da
--- /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;
+} 
\ No newline at end of file
diff --git a/Test/struct_parameter_after_non_struct.frag b/Test/struct_parameter_after_non_struct.frag
new file mode 100644
index 0000000000..3ed342b043
--- /dev/null
+++ b/Test/struct_parameter_after_non_struct.frag
@@ -0,0 +1,15 @@
+#version 400
+
+struct B {
+    int a;
+};
+
+// Test struct parameter after non-struct parameter (minimal regression test).
+void test1(B b, int c) {} // works.
+void test2(int c, B b) {} // should work.
+
+void main() {
+    B b = B(42);
+    test1(b, 10);
+    test2(10, b);
+} 
\ No newline at end of file
diff --git a/glslang/MachineIndependent/Scan.cpp b/glslang/MachineIndependent/Scan.cpp
index a29a6da25c..28b1fe13bb 100644
--- a/glslang/MachineIndependent/Scan.cpp
+++ b/glslang/MachineIndependent/Scan.cpp
@@ -836,12 +836,22 @@ 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; parenDepth = 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, array expression,
+            // or function parameter list.
+            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; parenDepth++; return LEFT_PAREN;
+        case ')':  afterType = false; inDeclaratorList = false; afterDeclarator = false; if (parenDepth > 0) parenDepth--; return RIGHT_PAREN;
         case '.':  field = true;        return DOT;
         case '!':                       return BANG;
         case '-':                       return DASH;
@@ -850,16 +860,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; parenDepth = 0; return LEFT_BRACE;
+        case '}':  inDeclaratorList = false; afterDeclarator = false; angleBracketDepth = 0; squareBracketDepth = 0; parenDepth = 0; return RIGHT_BRACE;
         case '\\':
             parseContext.error(loc, "illegal use of escape character", "\\", "");
             break;
@@ -1935,14 +1945,29 @@ int TScanContext::identifierOrType()
     if (field)
         return IDENTIFIER;
 
+    // If we see an identifier right after a type, this might be a declarator.
+    // But not in template parameters (inside angle brackets), array expressions (inside square brackets),
+    // or function parameters (inside parentheses)
+    if (afterType && angleBracketDepth == 0 && squareBracketDepth == 0 && parenDepth == 0) {
+        afterDeclarator = true;
+        afterType = false;
+        return IDENTIFIER;
+    }
+
     parserToken->sType.lex.symbol = parseContext.symbolTable.find(*parserToken->sType.lex.string);
     if ((afterType == false && afterStruct == false) && parserToken->sType.lex.symbol != nullptr) {
         if (const TVariable* variable = parserToken->sType.lex.symbol->getAsVariable()) {
             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;
             }
         }
diff --git a/glslang/MachineIndependent/ScanContext.h b/glslang/MachineIndependent/ScanContext.h
index 74b2b3c746..ce2145eb99 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), parenDepth(0) { }
     virtual ~TScanContext() { }
 
     static void fillInKeywordMap();
@@ -82,6 +82,11 @@ 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
+    int parenDepth;           // track nesting level of ( ) to detect function parameters
     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..0536ed4585
--- /dev/null
+++ b/gtests/StructName.cpp
@@ -0,0 +1,129 @@
+// Copyright (C) 2025 NVIDIA Corporation
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+#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, StructNameAsStructMember)
+{
+    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 parameter after non-struct parameter (minimal regression test).
+TEST_F(StructNameTest, StructParameterAfterNonStruct)
+{
+    const std::string inputFname = GlobalTestSettings.testRoot + "/struct_parameter_after_non_struct.frag";
+    std::string input;
+    tryLoadFile(inputFname, "input", &input);
+
+    EShMessages controls = DeriveOptions(Source::GLSL, Semantics::OpenGL, Target::AST);
+    GlslangResult result = compileAndLink("struct_parameter_after_non_struct.frag", input, "", controls,
+                                          glslang::EShTargetVulkan_1_0, glslang::EShTargetSpv_1_0);
+
+    // Should NOT have syntax errors related to struct types in function parameters.
+    EXPECT_EQ(result.linkingError.find("syntax error, unexpected IDENTIFIER"), std::string::npos);
+    EXPECT_EQ(result.shaderResults[0].output.find("syntax error, unexpected IDENTIFIER"), std::string::npos);
+
+    // Should compile successfully.
+    EXPECT_EQ(result.shaderResults[0].output.find("compilation errors"), std::string::npos);
+}
+
+// Test struct names as member names in declarator lists (original issue #3931) - More comprehensive patterns.
+TEST_F(StructNameTest, StructMemberDeclaratorLists)
+{
+    const std::string inputFname = GlobalTestSettings.testRoot + "/struct_member_declarator_lists.frag";
+    std::string input;
+    tryLoadFile(inputFname, "input", &input);
+
+    EShMessages controls = DeriveOptions(Source::GLSL, Semantics::OpenGL, Target::AST);
+    GlslangResult result = compileAndLink("struct_member_declarator_lists.frag", input, "", controls,
+                                          glslang::EShTargetVulkan_1_0, glslang::EShTargetSpv_1_0);
+
+    // Should NOT have the original syntax error from issue #3931.
+    EXPECT_EQ(result.linkingError.find("syntax error, unexpected TYPE_NAME, expecting IDENTIFIER"), std::string::npos);
+    EXPECT_EQ(result.shaderResults[0].output.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 function parameters with struct types (regression test for issue #4001).
+TEST_F(StructNameTest, StructFunctionParameters)
+{
+    const std::string inputFname = GlobalTestSettings.testRoot + "/struct_function_parameters.frag";
+    std::string input;
+    tryLoadFile(inputFname, "input", &input);
+
+    EShMessages controls = DeriveOptions(Source::GLSL, Semantics::OpenGL, Target::AST);
+    GlslangResult result = compileAndLink("struct_function_parameters.frag", input, "", controls,
+                                          glslang::EShTargetVulkan_1_0, glslang::EShTargetSpv_1_0);
+
+    // Should NOT have syntax errors related to struct types in function parameters.
+    EXPECT_EQ(result.linkingError.find("syntax error, unexpected IDENTIFIER"), std::string::npos);
+    EXPECT_EQ(result.shaderResults[0].output.find("syntax error, unexpected IDENTIFIER"), std::string::npos);
+
+    // Should compile successfully.
+    EXPECT_EQ(result.shaderResults[0].output.find("compilation errors"), std::string::npos);
+}
+
+// Test struct construction patterns - comprehensive patterns including struct constructors.
+TEST_F(StructNameTest, StructConstructionPatterns)
+{
+    const std::string inputFname = GlobalTestSettings.testRoot + "/struct_construction_patterns.frag";
+    std::string input;
+    tryLoadFile(inputFname, "input", &input);
+
+    EShMessages controls = DeriveOptions(Source::GLSL, Semantics::OpenGL, Target::AST);
+    GlslangResult result = compileAndLink("struct_construction_patterns.frag", input, "", controls,
+                                          glslang::EShTargetVulkan_1_0, glslang::EShTargetSpv_1_0);
+
+    // Should NOT have syntax errors in struct construction patterns.
+    EXPECT_EQ(result.linkingError.find("syntax error, unexpected IDENTIFIER"), std::string::npos);
+    EXPECT_EQ(result.shaderResults[0].output.find("syntax error, unexpected IDENTIFIER"), std::string::npos);
+
+    // Should NOT have the original syntax error from issue #3931.
+    EXPECT_EQ(result.linkingError.find("syntax error, unexpected TYPE_NAME, expecting IDENTIFIER"), std::string::npos);
+    EXPECT_EQ(result.shaderResults[0].output.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