Browse Source

Class inheritance (sign offs rebase) (#59)

* use an environment variable to locate program files.
+ verify early that pyyaml isn't missing. this causes 20 fails.
+ trim whitespace

* wip: class inheritance support. 1st broad source inspection pass.

* WIP: core of the change. activate deep lookup (resolve inherited names recursively).

* add miscelaneous utilities
+ add re-emission of interfaces
+ add emission of inheritance lists
+ change base holding from set to vector (to preserve order)
+ fixup base lookup scope (it was target class's scope, must be enclosing)
+ add a typeof case to exploit inheritance lookup

* Evolution of the lookup function to fixup seenats refering to inherited members, and also fully qualified idExpression that were bypassing lookup.

* Prepare some test cases to verify inheritance access understanding.
wip: advanced / verification of seenats not ready

* Fixup of forgotten function-parameter-semantic when that parameter is nameless.

* fixup of symbol dependency ordering: UDT internal variables that got declared prior to a nested type, ended up extracted ABOVE the definition of their own parent type.

* introduce class inheritance seenat advanced test

* add one OK test for the empty diamond case

* Add an error case test to make sure the multi concrete base inheritance correctly spawns a #17 error.

Signed-off-by: Vivien Oddou <[email protected]>
siliconvoodoo 2 years ago
parent
commit
02082bf1c8
35 changed files with 512 additions and 92 deletions
  1. 3 3
      Platform/Windows/src/DirectX12PlatformEmitter.cpp
  2. 35 9
      src/AzslcEmitter.cpp
  3. 12 4
      src/AzslcEmitter.h
  4. 2 0
      src/AzslcException.h
  5. 30 5
      src/AzslcKindInfo.h
  6. 13 1
      src/AzslcListener.cpp
  7. 1 0
      src/AzslcListener.h
  8. 2 2
      src/AzslcMain.cpp
  9. 7 1
      src/AzslcMangling.h
  10. 1 1
      src/AzslcReflection.cpp
  11. 1 1
      src/AzslcScopeTracker.cpp
  12. 41 25
      src/AzslcSemanticOrchestrator.cpp
  13. 1 1
      src/AzslcSemanticOrchestrator.h
  14. 34 8
      src/AzslcSymbolAggregator.cpp
  15. 3 2
      src/AzslcUtils.h
  16. 23 2
      src/GenericUtils.h
  17. 13 0
      tests/Advanced/inheritance-lookup-seenats.azsl
  18. 64 0
      tests/Advanced/inheritance-lookup-seenats.py
  19. 4 0
      tests/Emission/unnamed-arg-w-semantic.azsl
  20. 1 0
      tests/Emission/unnamed-arg-w-semantic.txt
  21. 16 0
      tests/Semantic/AsError/deep-inheritance-diamond-override.azsl
  22. 3 3
      tests/Semantic/AsError/inheritance-base-not-declared.azsl
  23. 1 1
      tests/Semantic/AsError/inheritance-base-not-interface.azsl
  24. 4 0
      tests/Semantic/AsError/inheritance-multi-concrete-bases.azsl
  25. 0 6
      tests/Semantic/AsError/interface-undeclared-identifier.azsl
  26. 27 0
      tests/Semantic/class-inheritance-hidden-inherited-member-access.azsl
  27. 18 0
      tests/Semantic/class-inheritance-inherited-member-access-sro.azsl
  28. 45 0
      tests/Semantic/class-inheritance.azsl
  29. 18 0
      tests/Semantic/deep-inheritance-symbol-access.azsl
  30. 14 0
      tests/Semantic/inheritance-diamond-ok.azsl
  31. 37 0
      tests/Semantic/multi-inheritance-symbol-shadowing-qualifiedresolution.azsl
  32. 15 1
      tests/Semantic/typeof-keyword.azsl
  33. 10 10
      tests/builder.py
  34. 8 1
      tests/testapp.py
  35. 5 5
      tests/testhelper.py

+ 3 - 3
Platform/Windows/src/DirectX12PlatformEmitter.cpp

@@ -90,11 +90,11 @@ namespace AZ::ShaderCompiler
 
             if (!descriptorTable.empty())
             {
-                rootAttrList.push_back(Decorate("            \"DescriptorTable(", Join(descriptorTable.begin(), descriptorTable.end()       , ", \" \\\n                            \""), ", visibility=SHADER_VISIBILITY_ALL)"));
+                rootAttrList.push_back(Decorate("            \"DescriptorTable(", Join(descriptorTable, ", \" \\\n                            \""), ", visibility=SHADER_VISIBILITY_ALL)"));
             }
             if (!samplerTable.empty())
             {
-                rootAttrList.push_back(Decorate("            \"DescriptorTable(", Join(samplerTable.begin(), samplerTable.end(), ", \" \\\n                            \""), ", visibility=SHADER_VISIBILITY_ALL)"));
+                rootAttrList.push_back(Decorate("            \"DescriptorTable(", Join(samplerTable, ", \" \\\n                            \""), ", visibility=SHADER_VISIBILITY_ALL)"));
             }
         }
 
@@ -123,7 +123,7 @@ namespace AZ::ShaderCompiler
 
         rootAttrList.insert(rootAttrList.begin(), "\"RootFlags(0)");
 
-        return Decorate("#define sig ", Join(rootAttrList.begin(), rootAttrList.end(), ", \" \\\n"), "\"\n\n");
+        return Decorate("#define sig ", Join(rootAttrList, ", \" \\\n"), "\"\n\n");
     }
     
 }

+ 35 - 9
src/AzslcEmitter.cpp

@@ -84,7 +84,7 @@ namespace AZ::ShaderCompiler
 
         if (!attr.m_argList.empty())
         {
-            out << "(" << Join(attr.m_argList.begin(), attr.m_argList.end(), ", ") << ")";
+            out << "(" << Join(attr.m_argList, ", ") << ")";
         }
         return out;
     }
@@ -122,6 +122,7 @@ namespace AZ::ShaderCompiler
             switch (iteratedSymbolKind)
             {
                 // top-level enums, structs and classes, as well as immediate-type-declaration enum/structs (`struct S{} s;`)
+            case Kind::Interface:
             case Kind::Struct:
             case Kind::Class:
             case Kind::Enum:
@@ -357,7 +358,7 @@ namespace AZ::ShaderCompiler
             }
         }
 
-        // DXC has made an error the definition of typedef in classes -> move them all to global
+        // DXC has made into an error the definition of typedef in classes -> move them all to global
         for (auto& [typeAliasUid, typeAliasInfo] : m_ir->GetOrderedSymbolsOfSubType_2<TypeAliasInfo>())
         {
             if (!IsGlobal(typeAliasUid.GetName()))
@@ -486,22 +487,48 @@ namespace AZ::ShaderCompiler
         EmitPreprocessorLineDirective(origSourceLine);
     }
 
-    void CodeEmitter::EmitStruct(const ClassInfo& classInfo, string_view structName, const Options& options)
+    string CodeEmitter::EmitInheritanceList(const ClassInfo& clInfo)
+    {
+        string hlsl = clInfo.HasAnyBases() ? " : " : "";
+        vector<string> mutatedBaseNames;
+        TransformCopy(clInfo.GetBases(), mutatedBaseNames,
+                      [&](const IdentifierUID& uid)
+                      {
+                          return GetTranslatedName(uid, UsageContext::ReferenceSite);
+                      });
+        hlsl += Join(mutatedBaseNames, ", ");
+        return hlsl;
+    }
+
+    void CodeEmitter::EmitStruct(const ClassInfo& classInfo, string_view structuredSymName, const Options& options)
     {
         EmitEmptyLinesToLineNumber(classInfo.GetOriginalLineNumber());
 
-        const bool hasName = (structName.length() > 0);
+        auto HlslStructuredDelcTagFromKind = [](Kind k)
+        {
+            switch (k)
+            {
+            case Kind::Struct: return "struct";
+            case Kind::Class: return "class";
+            case Kind::Interface: return "interface";
+            default: return " ";
+            }
+        };
+
+        const bool hasName = (structuredSymName.length() > 0);
         const auto tabs = "    ";
         if (hasName)
         {
-            EmitAllAttachedAttributes(IdentifierUID { QualifiedNameView{structName} });
-
-            m_out << (classInfo.m_kind == Kind::Struct ? "struct " : "class ") << GetTranslatedName(QualifiedNameView{structName}, UsageContext::DeclarationSite) << "\n{\n";
+            EmitAllAttachedAttributes(IdentifierUID { QualifiedNameView{structuredSymName} });
+            m_out << HlslStructuredDelcTagFromKind(classInfo.m_kind) << " "
+                  << GetTranslatedName(QualifiedNameView{structuredSymName}, UsageContext::DeclarationSite)
+                  << EmitInheritanceList(classInfo)
+                  << "\n{\n"; // conclusion of "class X : ::Stuff {"
         }
 
         for (const IdentifierUID& memberUid : classInfo.GetOrderedMembers())
         {
-            if (structName.empty() || m_translations.GetLandingScope(memberUid.GetName()) == structName)
+            if (structuredSymName.empty() || m_translations.GetLandingScope(memberUid.GetName()) == structuredSymName)
             {
                 auto& [uid, info] = *m_ir->GetIdAndKindInfo(memberUid.GetName());
                 if (info.IsKindOneOf(Kind::Class, Kind::Struct, Kind::Interface))
@@ -712,7 +739,6 @@ namespace AZ::ShaderCompiler
                                || emitAsDefinition && AlreadyEmittedFunctionDefinition(uid);
         bool undefinedFunction = funcSub.IsUndefinedFunction();
         if (riskDoubleEmission
-            || funcSub.m_isVirtual   // interface's methods (isVirtual) are a declarative construct of AZSL and don't appear in HLSL.
             || undefinedFunction && emitAsDefinition)
         {
             return;

+ 12 - 4
src/AzslcEmitter.h

@@ -142,6 +142,11 @@ namespace AZ::ShaderCompiler
                         }
                     }
 
+                    if (auto* semantic = param.m_semanticCtx)
+                    {
+                        m_out << " " + semantic->getText();
+                    }
+
                     if (param.m_defaultValueExpression)
                     {
                         EmitText(param.m_defaultValueExpression->getSourceInterval());
@@ -222,11 +227,14 @@ namespace AZ::ShaderCompiler
         mutable NewLineCounterStream m_out;
         void EmitEmptyLinesToLineNumber(size_t originalLineNumber) const;
 
-        // This template takes over the previous implementation of
-        // void GetTextInStream(misc::Interval interval, std::ostream& output) const override;
-        // The idea is that by using the template We only have to write the same code once
-        // whether We are using a regular std::ostream or an instance of NewLineCounterStream.
+        //! This template takes over the previous implementation of
+        //! void GetTextInStream(misc::Interval interval, std::ostream& output) const override;
+        //! The idea is that by using the template We only have to write the same code once
+        //! whether We are using a regular std::ostream or an instance of NewLineCounterStream.
         template <class StreamLike>
         void GetTextInStreamInternal(misc::Interval interval, StreamLike& output, bool emitNewLines) const;
+
+        //! This is a readability function for class emission code. Serves for HLSL declarator of classes
+        string EmitInheritanceList(const ClassInfo& clInfo);
     };
 }

+ 2 - 0
src/AzslcException.h

@@ -72,6 +72,8 @@ namespace AZ::ShaderCompiler
         ORCHESTRATOR_SRG_EXTENSION_HAS_DIFFERENT_SEMANTIC = 47u,
         ORCHESTRATOR_UNBOUNDED_RESOURCE_ISSUE = 48u,
         ORCHESTRATOR_UNKNOWN_OPTION_TYPE = 49u,
+        ORCHESTRATOR_CONSTANT_FOLDING_FAULT = 50u,
+        ORCHESTRATOR_TYPE_LOOKUP_FAULT = 51u,
 
 
         // Treat all compiler warnings as errors

+ 30 - 5
src/AzslcKindInfo.h

@@ -105,6 +105,29 @@ namespace AZ::ShaderCompiler
             m_ordered.insert(itor, newUid);
         }
 
+        bool HasBase(const IdentifierUID& query) const
+        {
+            return std::find(m_bases.begin(), m_bases.end(), query) != m_bases.end();
+        }
+
+        bool HasAnyBases() const
+        {
+            return !m_bases.empty();
+        }
+
+        void PushBase(const IdentifierUID& newBase)
+        {
+            if (!HasBase(newBase))
+            {
+                m_bases.push_back(newBase);
+            }
+        }
+
+        const vector<IdentifierUID>& GetBases() const
+        {
+            return m_bases;
+        }
+
         const vector<IdentifierUID>& GetMemberFields() const
         {
             return m_memberFields;
@@ -185,7 +208,6 @@ namespace AZ::ShaderCompiler
         }
 
         Kind                           m_kind;   // which of class/struct/interface/srgsemantic ? (repetition of data in the upper KindInfo)
-        unordered_set< IdentifierUID > m_bases;
 
         using DeclNode = variant< AstClassDeclNode*, AstStructDeclNode*, AstEnumDeclNode*, AstInterfaceDeclNode*, AstSRGSemanticDeclNode* >;
         DeclNode                       m_declNodeVt;
@@ -197,6 +219,7 @@ namespace AZ::ShaderCompiler
         unordered_set< IdentifierUID > m_members;      //!< Fast lookup
         vector< IdentifierUID >        m_memberFields; //!< Only the member fields, in order of declaration. All member fields are members.
         vector< IdentifierUID >        m_ordered;      //!< Ordered. all contained symbols
+        vector< IdentifierUID >        m_bases;
     };
     
     //! an extended type information gathers:
@@ -346,7 +369,7 @@ namespace AZ::ShaderCompiler
         // returns an ArrayDimensions struct const ref.
         inline const auto&         GetArrayDimensions() const;
         // Returns the line number, in the AZSL file, where this symbol is declared. 
-        inline size_t GetOriginalLineNumber () const;
+        inline size_t              GetOriginalLineNumber () const;
 
         AstUnnamedVarDecl*         m_declNode = nullptr;
         UnqualifiedName            m_identifier;
@@ -657,14 +680,15 @@ namespace AZ::ShaderCompiler
         }
 
         //! add a parameter
-        void PushParameter(IdentifierUID varName, const ExtendedTypeInfo& typeInfo, TypeQualifier typeQualifier, const std::vector<azslParser::ArrayRankSpecifierContext*>& arrayRankSpecifiers, AstVarInitializer* initCtx)
+        void PushParameter(IdentifierUID varName, const ExtendedTypeInfo& typeInfo, TypeQualifier typeQualifier, AstUnnamedVarDecl* unnamedCtx)
         {
             Parameter param;
             param.m_varId = varName;
             param.m_typeInfo = typeInfo;
             param.m_typeQualifier = typeQualifier;
-            param.m_arrayRankSpecifiers = arrayRankSpecifiers;
-            param.m_defaultValueExpression = initCtx;
+            param.m_semanticCtx = unnamedCtx->SemanticOpt;
+            param.m_arrayRankSpecifiers = unnamedCtx->ArrayRankSpecifiers;
+            param.m_defaultValueExpression = unnamedCtx->variableInitializer();
             m_parameters[m_currentList].push_back(param);
         }
 
@@ -756,6 +780,7 @@ namespace AZ::ShaderCompiler
             IdentifierUID m_varId;
             ExtendedTypeInfo m_typeInfo;
             TypeQualifier m_typeQualifier;
+            azslParser::HlslSemanticContext* m_semanticCtx = nullptr;
             std::vector<azslParser::ArrayRankSpecifierContext*> m_arrayRankSpecifiers;
             AstVarInitializer* m_defaultValueExpression = nullptr;
         };

+ 13 - 1
src/AzslcListener.cpp

@@ -64,7 +64,10 @@ namespace AZ::ShaderCompiler
     void SemaCheckListener::enterClassDefinition(azslParser::ClassDefinitionContext* ctx)
     {
         m_ir->m_sema.RegisterClass(ctx);
-        m_ir->m_scope.EnterScope(ctx->Name->getText(), ctx->LeftBrace()->getSourceInterval().a);
+        if (!ctx->baseList())  // if there is a base list, we can't enter the scope that early. it has to be at base list exit.
+        {
+            m_ir->m_scope.EnterScope(ctx->Name->getText(), ctx->LeftBrace()->getSourceInterval().a);
+        }
     }
 
     void SemaCheckListener::enterEnumDefinition(azslParser::EnumDefinitionContext* ctx)
@@ -278,9 +281,18 @@ namespace AZ::ShaderCompiler
 
     void SemaCheckListener::enterBaseList(azslParser::BaseListContext* ctx)
     {
+
         m_ir->m_sema.RegisterBases(ctx);
     }
 
+    void SemaCheckListener::exitBaseList(azslParser::BaseListContext* ctx)
+    {
+        // if there was a base list, we deferred entering in scope
+        auto* classDefCtx = polymorphic_downcast<AstClassDeclNode*>(ctx->parent);  // baseList is only used in that context
+        m_ir->m_scope.EnterScope(classDefCtx->Name->getText(),
+                                 classDefCtx->LeftBrace()->getSourceInterval().a);
+    }
+
     void SemaCheckListener::enterCompilerExtensionStatement(azslParser::CompilerExtensionStatementContext* ctx)
     {
         if (m_silentPrintExtensions)

+ 1 - 0
src/AzslcListener.h

@@ -46,6 +46,7 @@ namespace AZ::ShaderCompiler
         void exitFunctionParam(azslParser::FunctionParamContext* ctx) override;
         void enterNamedVariableDeclarator(azslParser::NamedVariableDeclaratorContext* ctx) override;
         void enterBaseList(azslParser::BaseListContext* ctx) override;
+        void exitBaseList(azslParser::BaseListContext* ctx) override;
         void enterCompilerExtensionStatement(azslParser::CompilerExtensionStatementContext* ctx) override;
         void enterGlobalAttribute(azslParser::GlobalAttributeContext* ctx) override;
         void enterAttachedAttribute(azslParser::AttachedAttributeContext* ctx) override;

+ 2 - 2
src/AzslcMain.cpp

@@ -21,9 +21,9 @@ namespace StdFs = std::filesystem;
 // Correspond to the supported version of the AZSL language.
 #define AZSLC_MAJOR "1"
 // For large features or milestones. Minor version allows for breaking changes. Existing tests can change.
-#define AZSLC_MINOR "7"
+#define AZSLC_MINOR "8"   // introduction of class inheritance
 // For small features or bug fixes. They cannot introduce breaking changes. Existing tests shouldn't change.
-#define AZSLC_REVISION "35" // Upgrade from Antlr 4.7.1 to Antlr 4.9.3
+#define AZSLC_REVISION "4"
 namespace AZ::ShaderCompiler
 {
     DiagnosticStream verboseCout;

+ 7 - 1
src/AzslcMangling.h

@@ -18,7 +18,7 @@ namespace AZ::ShaderCompiler
     //! Picture a sort of BOOST_STRONG_TYPEDEF. But that keeps compatibility with a 'view' type
     struct QualifiedName : string
     {
-        // I need to declare explicitly a default constructor, since the declaration of a copy constructor hereunder would otherwise delete it.
+        // Need to declare explicitly a default constructor, since the declaration of a copy constructor hereunder would otherwise delete it.
         QualifiedName() = default;
 
         // Provide implicit compatibility from string_view for sheer convenience
@@ -354,6 +354,12 @@ namespace AZ::ShaderCompiler
         return QualifiedNameView{GetParentName(string_view{path})};
     }
 
+    //! Overload for when you work with QualifiedName type
+    inline QualifiedNameView GetParentName(const QualifiedName& path)
+    {
+        return QualifiedNameView{GetParentName(QualifiedNameView{path})};
+    }
+
     struct PathPart
     {
         string_view m_slice;

+ 1 - 1
src/AzslcReflection.cpp

@@ -136,7 +136,7 @@ namespace AZ::ShaderCompiler
                 {
                     return false;
                 }
-                auto[semanticName, semanticIndex, isSystemValue] = ExtractHlslSemantic(hlslSemantic);
+                auto [semanticName, semanticIndex, isSystemValue] = ExtractHlslSemantic(hlslSemantic);
                 if (!BuildOMElement(jsonVal, attrVarInfo.m_typeInfoExt, semanticName.c_str(), semanticIndex, isSystemValue))
                 {
                     return false;

+ 1 - 1
src/AzslcScopeTracker.cpp

@@ -50,7 +50,7 @@ namespace AZ::ShaderCompiler
 
     void ScopeTracker::UpdateCurScopeUID()
     {
-        auto& [uid, Kind] = *m_symFinder(m_currentScopePath);
+        auto& [uid, _] = *m_symFinder(m_currentScopePath);
         // ↓ in this case please make sure you register the scope identifier before calling EnterScope. yes it is a sequential coupling antipattern, sorry for now
         assert(IsRooted(m_currentScopePath));
         m_currentScopeUID = uid;

+ 41 - 25
src/AzslcSemanticOrchestrator.cpp

@@ -606,7 +606,7 @@ namespace AZ::ShaderCompiler
         {
             // We need to register each newly registered parameter variable ID, in the list of the function subinfo too:
             auto& funcSub = GetCurrentScopeSubInfoAs<FunctionInfo>();
-            funcSub.PushParameter(uid, varInfo.m_typeInfoExt, varInfo.m_typeQualifier, varInfo.m_declNode->ArrayRankSpecifiers, ctx->variableInitializer());
+            funcSub.PushParameter(uid, varInfo.m_typeInfoExt, varInfo.m_typeQualifier, varInfo.m_declNode);
         }
 
         bool isExtern = !varInfo.StorageFlagIsLocalLinkage(global || enclosedBySRG);
@@ -664,7 +664,7 @@ namespace AZ::ShaderCompiler
         ArrayDimensions arrayDims;
         TryFoldArrayDimensions(ctx->unnamedVariableDeclarator(), arrayDims);
         auto paramType = CreateExtendedTypeInfo(ctx->type(), arrayDims, Packing::MatrixMajor::Default);
-        GetCurrentScopeSubInfoAs<FunctionInfo>().PushParameter({}, paramType, typeQualifier, ctx->unnamedVariableDeclarator()->ArrayRankSpecifiers, ctx->unnamedVariableDeclarator()->variableInitializer());
+        GetCurrentScopeSubInfoAs<FunctionInfo>().PushParameter({}, paramType, typeQualifier, ctx->unnamedVariableDeclarator());
     }
 
     // Helper to avoid code redundancy for a message that is used in three different places.
@@ -914,6 +914,11 @@ namespace AZ::ShaderCompiler
     {
         using namespace std::string_literals;
 
+        // reconstruct the "/C" in "class C : b0, b1..." (since the current scope is not yet /C)
+        auto* classDefCtx = polymorphic_downcast<AstClassDeclNode*>(ctx->parent);
+        IdentifierUID targetClassUid{MakeFullyQualified(UnqualifiedNameView{classDefCtx->Name->getText()})};
+        auto* targetClassInfo = m_symbols->GetAsSub<ClassInfo>(targetClassUid);
+
         for (auto& idexpr : ctx->idExpression())
         {
             UnqualifiedName baseName = ExtractNameFromIdExpression(idexpr);
@@ -923,8 +928,8 @@ namespace AZ::ShaderCompiler
                 ThrowAzslcOrchestratorException(ORCHESTRATOR_UNSPECIFIED_BASE_SYMBOL,
                     ctx->start, ConcatString("Base symbol "s, baseName, " not found"));
             }
-            auto& curClassInfo = GetCurrentScopeSubInfoAs<ClassInfo>();
-            curClassInfo.m_bases.emplace(baseSymbol->first);
+            // register base in the class info IR
+            targetClassInfo->PushBase(baseSymbol->first);
         }
     }
 
@@ -1352,11 +1357,12 @@ namespace AZ::ShaderCompiler
         // Get iterator into the symbol database from current scope name. (current scope should be the currently closing class)
         // Access the KindInfo from iter->second, and "cast" the `anyInfo` variant to ClassInfo:
         auto& classSubInfo = GetCurrentScopeSubInfoAs<ClassInfo>();
+        // this class original AST node:
+        auto declNode = get<AstClassDeclNode*>(classSubInfo.m_declNodeVt);
         // Semantic validation. Iterate each base UID as registered in the ClassInfo:
-        for (auto b : classSubInfo.m_bases)
+        int concreteBase = 0; // this can only be 0 or 1.
+        for (auto b : classSubInfo.GetBases())
         {
-            // this class original AST node:
-            auto declNode = get<AstClassDeclNode*>(classSubInfo.m_declNodeVt);
             // get line location diagnostic message of its declaration keyword in source:
             verboseCout << "  base: " << b.m_name << "\n";
             // Fetch the base from name in the database
@@ -1364,19 +1370,30 @@ namespace AZ::ShaderCompiler
             assert(infoBase); // can't be undeclared since it already threw an error in RegisterBases.
             // wannabe is "want-to-be-a-base"
             auto& [baseUid, wannabeInfo] = *infoBase;
-            if (wannabeInfo.GetKind() != Kind::Interface)
+            if (!wannabeInfo.IsKindOneOf(Kind::Interface, Kind::Class))
             {
                 ThrowAzslcOrchestratorException(ORCHESTRATOR_INVALID_INTERFACE, declNode->Class()->getSymbol(),
-                    ConcatString("base ", baseUid.m_name, " is not an interface (it is a "s,
+                    ConcatString("base ", baseUid.m_name, " is not an interface or class (it is a "s,
                         Kind::ToStr(wannabeInfo.GetKind()).data(), ")"));
             }
-            auto& baseInterfaceInfo = wannabeInfo.GetSubRefAs<ClassInfo>();
-            for (auto basemember : baseInterfaceInfo.GetOrderedMembers())
-            {  // Check that any member present in base is present in this class
-                if (!classSubInfo.HasMember(basemember.GetNameLeaf()))
-                {
-                    ThrowAzslcOrchestratorException(ORCHESTRATOR_CLASS_REDEFINE, declNode->Class()->getSymbol(),
-                        ConcatString("class ", m_scope->m_currentScopeUID.m_name, " does not redefine ", basemember.m_name));
+            concreteBase += wannabeInfo.GetKind() == Kind::Class;
+            if (concreteBase > 1)
+            {
+                ThrowAzslcOrchestratorException(ORCHESTRATOR_INVALID_INTERFACE, declNode->Class()->getSymbol(),
+                                                ConcatString("class ", declNode->Name->getText(),
+                                                             " has multiple concrete bases. Only 1 concrete base allowed"s));
+            }
+            // verify interfaces full implementation
+            if (wannabeInfo.GetKind() == Kind::Interface)
+            {
+                auto& baseInfoAsClass = wannabeInfo.GetSubRefAs<ClassInfo>();
+                for (auto basemember : baseInfoAsClass.GetOrderedMembers())
+                {  // Check that any member present in base is present in this class
+                    if (!classSubInfo.HasMember(basemember.GetNameLeaf()))
+                    {
+                        ThrowAzslcOrchestratorException(ORCHESTRATOR_CLASS_REDEFINE, declNode->Class()->getSymbol(),
+                                                        ConcatString("class ", m_scope->m_currentScopeUID.m_name, " does not redefine ", basemember.m_name));
+                    }
                 }
             }
         }
@@ -1398,8 +1415,7 @@ namespace AZ::ShaderCompiler
             // let's do a bit of sanity check on that symbol
             Kind baseKind = baseFuncKind.GetKind();
             if (baseKind != Kind::Function)
-            {   // today, it is impossible to reach that diagnostic, since the grammar doesn't allow it.
-                // but we are envisioning Properties as a future possible Kind in interfaces.
+            {
                 auto baseKindStr = Kind::ToStr(baseKind).data();
                 ThrowAzslcOrchestratorException(ORCHESTRATOR_HIDING_SYMBOL_BASE, ctx->Identifier()->getSymbol(),
                     ConcatString("function ", thisFuncId.m_name, " is hiding a symbol of a base, that is not of Function kind, but is ", baseKindStr));
@@ -1671,20 +1687,20 @@ namespace AZ::ShaderCompiler
         auto maybeSymbol = LookupSymbol(uqName);
         if (!maybeSymbol)
         {
-            ThrowAzslcOrchestratorException(ORCHESTRATOR_DEPORTED_METHOD_DEFINITION, idExp->start,
+            ThrowAzslcOrchestratorException(ORCHESTRATOR_CONSTANT_FOLDING_FAULT, idExp->start,
                 ConcatString("in expected constant expression: identifier ", uqName, " not found"));
         }
         auto& [id, symbol] = *maybeSymbol;
         auto what = symbol.GetKind();
         if (what != Kind::Variable)
         {
-            ThrowAzslcOrchestratorException(ORCHESTRATOR_DEPORTED_METHOD_DEFINITION, idExp->start,
+            ThrowAzslcOrchestratorException(ORCHESTRATOR_CONSTANT_FOLDING_FAULT, idExp->start,
                 ConcatString("in expected constant expression: identifier ", uqName, " did not refer to a variable, but a ", Kind::ToStr(what).data()));
         }
         auto const& var = symbol.GetSubRefAs<VarInfo>();
         if (holds_alternative<monostate>(var.m_constVal))
         {
-            ThrowAzslcOrchestratorException(ORCHESTRATOR_DEPORTED_METHOD_DEFINITION, idExp->start,
+            ThrowAzslcOrchestratorException(ORCHESTRATOR_CONSTANT_FOLDING_FAULT, idExp->start,
                 ConcatString("in expected constant expression: variable ", id.m_name, " couldn't be folded to a constant (tip: use --semantic --verbose to diagnose why)"));
         }
         return var.m_constVal;
@@ -1822,7 +1838,7 @@ namespace AZ::ShaderCompiler
         {
             if (policy == OnNotFoundOrWrongKind::Diagnose)
             {
-                ThrowAzslcOrchestratorException(ORCHESTRATOR_DEPORTED_METHOD_DEFINITION,
+                ThrowAzslcOrchestratorException(ORCHESTRATOR_TYPE_LOOKUP_FAULT,
                     sourceline, none, ConcatString(" type ", string{ typeName }, " requested but not found."));
             }
             else
@@ -1838,7 +1854,7 @@ namespace AZ::ShaderCompiler
         {
             if (policy == OnNotFoundOrWrongKind::Diagnose)
             {
-                ThrowAzslcOrchestratorException(ORCHESTRATOR_DEPORTED_METHOD_DEFINITION,
+                ThrowAzslcOrchestratorException(ORCHESTRATOR_TYPE_LOOKUP_FAULT,
                     sourceline, none, ConcatString(" type ", typeName.data(),
                         " requested but found as ", Kind::ToStr(kind.GetKind()).data()));
             }
@@ -1916,11 +1932,11 @@ namespace AZ::ShaderCompiler
             // get currently parsed class info:
             auto& curClassInfo = GetCurrentParentScopeSubInfoAs<ClassInfo>();
             // look for a match in any base
-            for (const IdentifierUID& base : curClassInfo.m_bases)
+            for (const IdentifierUID& base : curClassInfo.GetBases())
             {
                 auto maybeBaseClass = m_symbols->GetIdAndKindInfo(base.m_name);
                 if (maybeBaseClass
-                    && maybeBaseClass->second.GetKind() == Kind::Interface)  // bases must be interfaces but we don't assume it's enforced prior to calls to this function
+                    && maybeBaseClass->second.IsKindOneOf(Kind::Interface, Kind::Class))  // bases must be interfaces or classes, but we don't assume it's enforced prior to calls to this function
                 {
                     const auto& baseClassInfo = maybeBaseClass->second.GetSubRefAs<ClassInfo>();
                     bool baseHasSameNameMember = baseClassInfo.HasMember(hidingCandidate.GetNameLeaf());

+ 1 - 1
src/AzslcSemanticOrchestrator.h

@@ -299,7 +299,7 @@ namespace AZ::ShaderCompiler
             {
                 if (idkind->second.GetKind() == Kind::TypeAlias)
                 {
-                    // recurse until not an alias !
+                    // recurse until not an alias!
                     typeRef = LookupType(UnqualifiedNameView{GetTypeName(idkind)}, policy);
                 }
                 else

+ 34 - 8
src/AzslcSymbolAggregator.cpp

@@ -112,7 +112,8 @@ namespace AZ::ShaderCompiler
         if (IsRooted(name))
         {   // It is possible that fully-qualified symbols find their way inside unqualified-tainted names.
             // For the reason mentioned in the comment decorating ExtractNameFromIdExpression() function. please refer.
-            return GetIdAndKindInfo(QualifiedNameView{name});
+            // Even fully qualified inputs must go through lookup resolution (to solve inherited access)
+            return LookupSymbol(QualifiedNameView{"/"}, UnqualifiedNameView{Slice(name, 1, -1)});
         }
         // try as floating symbol in priority (predefined are found at any scope)
         IdAndKind* got = GetIdAndKindInfo(QualifiedName{"?"s + name.data()});
@@ -125,6 +126,14 @@ namespace AZ::ShaderCompiler
         assert(IsRooted(scope));
         // Iterative lookup of the closest reachable symbol
         // by going further toward global.
+        // e.g try to locate: /Typ/Sub/Sym/name; if not found: /Typ/Sub/name; if not found: /Typ/name; ...
+        // this is the classic symbol shadowing scheme: closer symbols (less qualification distance) shadow more "global" symbols.
+        //   say:
+        //      int a;
+        //      class C {
+        //          int a;
+        //          void f() { a; }  // refers to /C/a  but would refer to /a if `C` didn't have a member a. /C/a shadows /a
+        //      };
         string_view path = scope;
         bool exit = false;
         do
@@ -132,6 +141,18 @@ namespace AZ::ShaderCompiler
             auto attempt = QualifiedName{JoinPath(path, name)};
             got = GetIdAndKindInfo(attempt);
             exit = path == "/";
+            if (!got)
+            {
+                if (auto* scopeAsClass = GetAsSub<ClassInfo>(IdentifierUID{GetParentName(attempt)})) // get enclosing class
+                {
+                    // classes need deep lookup, because may have bases. classes don't save in the symbol-table all the fields they render accessible.
+                    // Because multiple bases (upways and sideways) can shadow each-other's fields; caching inheritated fields would require complicated mangling.
+                    for (auto it = scopeAsClass->GetBases().begin(); it != scopeAsClass->GetBases().end() && !got; ++it)
+                    {
+                        got = LookupSymbol(it->GetName(), ExtractLeaf(attempt));
+                    }
+                }
+            }
             path = LevelUp(path);
         } while (!got && !exit);
         return got;
@@ -226,11 +247,13 @@ namespace AZ::ShaderCompiler
     {
         auto disambiguatorChar = '#';
         // query for symbol kind; because that function is specific to this algorithm, it's ok locally only.
-        auto isFunctionOrVariable = [this, disambiguatorChar](string_view name)
+        auto isFunctionOrVariableOrType = [this, disambiguatorChar](string_view name)
         {
             name = Slice(name, 0, name.find_first_of(disambiguatorChar));
             KindInfo& ki = GetIdAndKindInfo(QualifiedNameView{name})->second;
-            return std::make_pair(ki.IsKindOneOf(Kind::Function, Kind::OverloadSet), ki.IsKindOneOf(Kind::Variable));
+            return std::make_tuple(ki.IsKindOneOf(Kind::Function, Kind::OverloadSet),
+                                   ki.IsKindOneOf(Kind::Variable),
+                                   IsKindOneOfTypeRelated(ki.GetKind()));
         };
         // instanciate an empty solver and fill it up with the elastic symbols from the aggregator to reorder them
         DependencySolver<IdentifierUID, 50_maxdep_pernode> solver;
@@ -266,11 +289,14 @@ namespace AZ::ShaderCompiler
                 }
                 // establish a horizontal link between symbols of the same level to preserve the apparition order
                 bool sameParentAsLast = GetParentName(disambiguated.GetName()) == GetParentName(lastSymbolAtCurrentLevel.top().GetName());
-                bool lastIsFunction = isFunctionOrVariable(lastSymbolAtCurrentLevel.top().GetName()).first;
+                bool parentIsT = std::get<2>(isFunctionOrVariableOrType(GetParentName(disambiguated.GetName())));
+                bool lastIsFunction = std::get<0>(isFunctionOrVariableOrType(lastSymbolAtCurrentLevel.top().GetName()));
+                bool curIsT = std::get<2>(isFunctionOrVariableOrType(disambiguated.GetName()));
+                bool isNestedType = curIsT && symbolDepth > 0;
                 // verifying !lastIsFunction, permits to break dependency cycles.
-                if (sameParentAsLast && !lastIsFunction)
+                if (sameParentAsLast && !lastIsFunction && !(isNestedType && parentIsT))  // in "class C { int a; struct S{}; };"  `S` cannot depend on `a` otherwise `a` is pulled out of C
                 {
-                    solver.AddDependency(disambiguated, lastSymbolAtCurrentLevel.top());
+                    solver.AddDependency(disambiguated, lastSymbolAtCurrentLevel.top());  // make link:  ● 'g_fog' ← ● 'class C'
                 }
                 lastSymbolAtCurrentLevel.top() = disambiguated;
             }
@@ -280,10 +306,10 @@ namespace AZ::ShaderCompiler
                 // establish vertical links
                 string path;
                 IdentifierUID parent;
-                ForEachPathPart(disambiguated.GetName(), [this, &solver, &path, &parent, &isFunctionOrVariable](PathPart part)
+                ForEachPathPart(disambiguated.GetName(), [this, &solver, &path, &parent, &isFunctionOrVariableOrType](PathPart part)
                                 {
                                     path = JoinPath(path, part.m_slice);
-                                    auto [isFunc, isVar] = isFunctionOrVariable(path);
+                                    auto [isFunc, isVar, _] = isFunctionOrVariableOrType(path);
                                     IdentifierUID current{QualifiedNameView{path}};
                                     bool parentIsEmptyOrRoot = parent.IsEmpty() || parent.GetName() == "/";
                                     if (!parentIsEmptyOrRoot)

+ 3 - 2
src/AzslcUtils.h

@@ -310,8 +310,8 @@ namespace AZ::ShaderCompiler
         string ToString(const string& prefix = "[", const string& separator = "][", const string& suffix = "]") const
         {
             vector<string> asStrs;
-            std::transform(m_dimensions.begin(), m_dimensions.end(), std::back_inserter(asStrs),
-                           [](int d) -> string { return d == Unknown ? "?" : d == Unbounded ? "" : std::to_string(d); });
+            TransformCopy(m_dimensions, asStrs,
+                          [](int d) -> string { return d == Unknown ? "?" : d == Unbounded ? "" : std::to_string(d); });
             return asStrs.empty() ? "" : prefix + Join(asStrs.begin(), asStrs.end(), separator) + suffix;
         }
 
@@ -1184,6 +1184,7 @@ namespace AZ::ShaderCompiler
         ExtractedTypeExt m_genericParam; // note: could be made recursive one day. use unique_ptr here
         ArrayDimensions  m_genericDimensions; // can expand generic dimensions too later. for MSTexture, IO patch, matrix<t,N,M> etc...
     };
+
     // Compose middleEnd configurations, wraps minimal user options from command line.
     struct MiddleEndConfiguration
     {

+ 23 - 2
src/GenericUtils.h

@@ -209,7 +209,7 @@ namespace AZ
         return haystack;
     }
 
-    template <typename Iter>
+    template< typename Iter >
     string Join(Iter begin, Iter end, string_view separator = "")
     {
         if (begin == end)
@@ -230,13 +230,34 @@ namespace AZ
         return std::find_if(begin, end, p) != end;
     }
 
-    //! closest possible form of python's `in` keyword
+    /// argument in rangeV3-style version:
+    template< typename Container >
+    string Join(const Container& c, string_view separator = "")
+    {
+        return Join(c.begin(), c.end(), separator);
+    }
+
+    /// argument in rangeV3-style version:
+    template< typename Container, typename Predicate >
+    bool Contains(const Container& c, Predicate p)
+    {
+        return Contains(c.begin(), c.end(), p);
+    }
+
+    /// closest possible form of python's `in` keyword
     template< typename Element, typename Container >
     bool IsIn(const Element& element, const Container& container)
     {
         return std::find(container.begin(), container.end(), element) != container.end();
     }
 
+    /// generate a new container with copy-and-mutated elements
+    template< typename Container, typename ContainerOut, typename Functor >
+    void TransformCopy(const Container& in, ContainerOut& out, Functor mutator)
+    {
+        std::transform(in.begin(), in.end(), std::back_inserter(out), mutator);
+    }
+
     inline string Unescape(string_view escapedText)
     {
         std::stringstream out;

+ 13 - 0
tests/Advanced/inheritance-lookup-seenats.azsl

@@ -0,0 +1,13 @@
+class A
+{
+	float4 color : COLOR;
+	class A  // this creates a trap of spurious lookup find if we search for A in B when we ought to search for A in /
+	{
+	    int4 i;
+	};
+};
+// note: in C++ it's a forbidden construct. gcc: error: 'A::A' has the same name as the class in which it is declared
+
+class B : A
+{
+};

+ 64 - 0
tests/Advanced/inheritance-lookup-seenats.py

@@ -0,0 +1,64 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+"""
+Copyright (c) Contributors to the Open 3D Engine Project.
+For complete copyright and license terms please see the LICENSE at the root of this distribution.
+
+SPDX-License-Identifier: Apache-2.0 OR MIT
+"""
+import sys
+import os
+from clr import *
+sys.path.append("..")
+import testfuncs
+
+def execTest(thefile, compilerPath, silent):
+    '''return number of successes'''
+    symbols, ok = testfuncs.buildAndGetSymbols(thefile, compilerPath, silent)
+    # check the specific stuff we want to verify with this test
+    if ok:
+        try:
+            ok = symbols["Symbol '/A/A'"]['kind'] == 'Class'
+            
+            ok2 = symbols["Symbol '/A/A'"]["references"] == None   # no references for A::A
+            
+            ok3 = len(symbols["Symbol '/A'"]["references"]) == 1
+
+            ok4 = symbols["Symbol '/A'"]["references"][0]["line"] == 11  # /A has one ref in the baselist of B line 11
+
+            if not silent:
+                if not ok:
+                    print (fg.RED+ "Couldn't verify symbol /A/A is a Class"+ style.RESET_ALL)
+
+                if not ok2:
+                    print (fg.RED+ "Couldn't verify symbol /A/A is ot referenced"+ style.RESET_ALL)
+
+                if not ok3:
+                    print (fg.RED+ "Couldn't verify /A has 1 reference"+ style.RESET_ALL)
+
+                if not ok4:
+                    print (fg.RED+ "Couldn't verify /A's seenat is at line 11"+ style.RESET_ALL)
+
+            ok = ok and ok2 and ok3 and ok4
+
+        except Exception as e:
+            print (fg.RED+ "Err: dumpsym didn't match expectations"+ style.RESET_ALL, e)
+            ok = False
+    return ok
+
+result = 0  # to define for sub-tests
+resultFailed = 0
+def doTests(compiler, silent, azdxcpath):
+    global result
+    global resultFailed
+
+    # Working directory should have been set to this script's directory by the calling parent
+    # You can get it once doTests() is called, but not during initialization of the module,
+    #  because at that time it will still be set to the working directory of the calling script
+    workDir = os.getcwd()
+
+    if execTest(os.path.join(workDir, "inheritance-lookup-seenats.azsl"), compiler, silent): result += 1
+    else: resultFailed += 1
+
+if __name__ == "__main__":
+    print ("please call from testapp.py")

+ 4 - 0
tests/Emission/unnamed-arg-w-semantic.azsl

@@ -0,0 +1,4 @@
+float4 m(float4 : COLOR) : SV_Target0
+{
+    return c.color;
+}

+ 1 - 0
tests/Emission/unnamed-arg-w-semantic.txt

@@ -0,0 +1 @@
+" float4 m ( float4 : COLOR ) : SV_Target0 "

+ 16 - 0
tests/Semantic/AsError/deep-inheritance-diamond-override.azsl

@@ -0,0 +1,16 @@
+interface I
+{
+    void f();
+};
+
+class A : I
+{
+    void f();
+};
+
+class B : A, I
+{
+    void f();  // overrides A::f and I::F (with the latest mandatory)
+};
+
+// Semantic error #34: Found multiple symbols hidden by /B/f() in bases of /B. First was /A/f(), now also found in /I.

+ 3 - 3
tests/Semantic/AsError/inheritance-base-not-declared.azsl

@@ -1,6 +1,6 @@
-class Child : InexistentInterface
-{  
+class s : I_am_undeclared
+{
 };
 
-// Semantic Error 16: line 1::12 'Base symbol InexistentInterface not found'
+// Semantic Error 16: line 1::8 'Base symbol I_am_undeclared not found'
 // #EC 16

+ 1 - 1
tests/Semantic/AsError/inheritance-base-not-interface.azsl

@@ -6,5 +6,5 @@ class child : i1, s1
 {  
 };
 
-// Semantic Error 17: line 5::0 'base /s1 is not an interface (it is a Struct)'
+// Semantic Error 17: line 5::0 'base /s1 is not an interface or class (it is a Struct)'
 // #EC 17

+ 4 - 0
tests/Semantic/AsError/inheritance-multi-concrete-bases.azsl

@@ -0,0 +1,4 @@
+class A{};
+class B{};
+class C : A, B {};  // Semantic error 17: class C has multiple concrete bases. Only 1 concrete base allowed
+// #EC 17

+ 0 - 6
tests/Semantic/AsError/interface-undeclared-identifier.azsl

@@ -1,6 +0,0 @@
-class s : I_am_undeclared
-{
-};
-
-// Semantic Error 16: line 1::8 'Base symbol I_am_undeclared not found'
-// #EC 16

+ 27 - 0
tests/Semantic/class-inheritance-hidden-inherited-member-access.azsl

@@ -0,0 +1,27 @@
+class A
+{
+    bool a;
+};
+
+interface A2
+{
+    int a();
+};
+
+class B : A, A2
+{
+    int b;
+    int a();
+};
+
+static B b, b2;
+
+// reveal access to hidden inherited fields using base-qualification of MAE's RHS:
+__azslc_print_message("@check predicate ");
+__azslc_print_symbol(typeof(b.A::a), __azslc_prtsym_least_qualified);
+__azslc_print_message(" == 'bool'\n");
+
+// using full base-qualification or RHS:
+__azslc_print_message("@check predicate ");
+__azslc_print_symbol(typeof(b.::A::a), __azslc_prtsym_least_qualified);
+__azslc_print_message(" == 'bool'\n");

+ 18 - 0
tests/Semantic/class-inheritance-inherited-member-access-sro.azsl

@@ -0,0 +1,18 @@
+class A
+{
+	int a;
+};
+
+class B : A
+{
+};
+
+// access through scope resolution operator, but looked up from current scope
+__azslc_print_message("@check predicate ");
+__azslc_print_symbol(typeof(B::a), __azslc_prtsym_least_qualified);
+__azslc_print_message(" == 'int'\n");
+
+// same with full qualification (current scope ignored)
+__azslc_print_message("@check predicate ");
+__azslc_print_symbol(typeof(::B::a), __azslc_prtsym_least_qualified);
+__azslc_print_message(" == 'int'\n");

+ 45 - 0
tests/Semantic/class-inheritance.azsl

@@ -0,0 +1,45 @@
+class SurfaceData_BasePBR
+{   
+    precise float3 position;    //!< Position in world-space
+    float3 normal;              //!< Normal in world-space
+    float3 vertexNormal;        //!< Vertex normal in world-space
+    float3 baseColor;           //!< Surface base color
+    float metallic;             //!< Surface metallic property
+    //! Sets albedo, base color, specularF0, and metallic properties using metallic workflow
+    void SetAlbedoAndSpecularF0(float3 baseColor, float specularF0Factor, float metallic);
+    
+    float roughnessLinear;      //!< Perceptually linear roughness value authored by artists. Must be remapped to roughnessA before use
+    float roughnessA;           //!< Actual roughness value ( a.k.a. "alpha roughness") to be used in microfacet calculations
+    float roughnessA2;          //!< Alpha roughness ^ 2 (i.e. roughnessA * roughnessA), used in GGX, cached here for performance
+
+    //! Surface lighting data
+    float3 albedo;              //!< Albedo color of the non-metallic material, will be multiplied against the diffuse lighting value
+    float3 specularF0;          //!< Fresnel f0 spectral value of the surface
+};
+
+interface ISpecRough
+{
+    //! Applies specular anti-aliasing to roughnessA2
+    void ApplySpecularAA();
+
+    //! Calculates roughnessA and roughnessA2 after roughness has been set
+    void CalculateRoughnessA();
+};
+
+class SurfaceData_NewPBR : SurfaceData_BasePBR, ISpecRough
+{
+    float3 emissiveLighting;        //!< Emissive lighting
+    float diffuseAmbientOcclusion;  //!< Diffuse ambient occlusion factor - [0, 1] :: [Dark, Bright]
+    float specularOcclusion;        //!< Specular occlusion factor - [0, 1] :: [Dark, Bright]
+    
+    void ApplySpecularAA() {}
+    void CalculateRoughnessA() {}
+};
+
+
+float4 PSMain(PSInput input) : SV_TARGET
+{
+    SurfaceData_NewPBR obj;
+    obj.albedo.x = 1;
+	return float4(obj.albedo, 1);
+}

+ 18 - 0
tests/Semantic/deep-inheritance-symbol-access.azsl

@@ -0,0 +1,18 @@
+class A0
+{
+    int16_t a;
+};
+
+class A1 : A0
+{
+};
+
+class A2 : A1
+{
+};
+
+static A2 g;
+
+__azslc_print_message("@check predicate ");
+__azslc_print_symbol(typeof(g.a), __azslc_prtsym_least_qualified);
+__azslc_print_message(" == 'int16_t'\n");

+ 14 - 0
tests/Semantic/inheritance-diamond-ok.azsl

@@ -0,0 +1,14 @@
+interface I
+{
+};
+
+class A : I
+{
+};
+
+class B : A, I
+{
+};
+
+// DXC complains with warning: direct base '::I' is inaccessible due to ambiguity: B -> ::A -> ::I
+// but azslc tolerates that input

+ 37 - 0
tests/Semantic/multi-inheritance-symbol-shadowing-qualifiedresolution.azsl

@@ -0,0 +1,37 @@
+class A0
+{
+    int16_t a;
+};
+
+class A : A0
+{
+    int a;  
+};
+
+interface A2
+{
+    float a();  
+};
+
+class B : A, A2
+{
+    dword a();
+};
+
+static B b;
+
+__azslc_print_message("@check predicate ");
+__azslc_print_symbol(typeof(b.A0::a), __azslc_prtsym_least_qualified);
+__azslc_print_message(" == 'int16_t'\n");
+
+__azslc_print_message("@check predicate ");
+__azslc_print_symbol(typeof(b.A::a), __azslc_prtsym_least_qualified);
+__azslc_print_message(" == 'int'\n");
+
+__azslc_print_message("@check predicate ");
+__azslc_print_symbol(typeof(b.A2::a()), __azslc_prtsym_least_qualified);
+__azslc_print_message(" == 'float'\n");
+
+__azslc_print_message("@check predicate ");
+__azslc_print_symbol(typeof(b.a()), __azslc_prtsym_least_qualified);
+__azslc_print_message(" == 'dword'\n");

+ 15 - 1
tests/Semantic/typeof-keyword.azsl

@@ -17,6 +17,15 @@ class top
 
 top gettop();
 
+class A
+{
+	int a;
+};
+
+class B : A
+{
+};
+
 struct Half{};
 struct Double{};
 half   returnThings(Half);
@@ -177,6 +186,11 @@ void h()
     __azslc_print_symbol(typeof(top::inner), __azslc_prtsym_fully_qualified);
     __azslc_print_message(" == '/top/inner'\n");
 
+	// class inheritance: parent member access using scope resolution operator
+    __azslc_print_message("@check predicate ");
+    __azslc_print_symbol(typeof(B::a), __azslc_prtsym_least_qualified);
+    __azslc_print_message(" == 'int'\n");
+
     // function call, the return type is returned
     __azslc_print_message("@check predicate ");
     __azslc_print_symbol(typeof(gettop()), __azslc_prtsym_least_qualified);
@@ -424,7 +438,7 @@ void h()
     __azslc_print_message("@check predicate ");
     __azslc_print_symbol(typeof(uint()), __azslc_prtsym_least_qualified);
     __azslc_print_message(" == 'uint'\n");
-    
+
     // comma expression:
     int INTVAR;
     double DOUBLEVAR;

+ 10 - 10
tests/builder.py

@@ -39,7 +39,7 @@ def findDXC(silent):
         if not silent: print (fg.YELLOW+ style.BRIGHT+ "Will only test dxc.exe on Windows 10."+ style.RESET_ALL)
         return None
 
-    windowsKitsDir = "C:/Program Files (x86)/Windows Kits/10/bin"
+    windowsKitsDir = os.path.expandvars("%ProgramFiles(x86)%/Windows Kits/10/bin")
     if not os.path.isdir(windowsKitsDir):
         if not silent: print (fg.YELLOW+ style.BRIGHT+ "Expected {}, but did not find it.".format(windowsKitsDir)+ style.RESET_ALL)
         return None
@@ -54,7 +54,7 @@ def findDXC(silent):
         return None
 
     return dxcLocations[0]
-    
+
 '''Returns the path to the Spirv-Cross compiler, or None if it can't be found'''
 def findSpirvCross(spirvCrossPath, silent):
     if not os.path.isdir(spirvCrossPath):
@@ -126,7 +126,7 @@ def buildDXC(thefile, compilerPath, silent, extraIncList, azslcArgs, dxcArgs, az
     if os.path.exists(sbinPS):  os.remove(sbinPS)
 
     stdout, ok = testfuncs.buildAndGet(thefile, compilerPath, silent, azslcArgs)
-    if not ok: 
+    if not ok:
         if not silent: print (fg.RED+ style.BRIGHT+ "Failed to generate .hlsl file with AZSLc."+ style.RESET_ALL)
         return buildFailed
 
@@ -134,7 +134,7 @@ def buildDXC(thefile, compilerPath, silent, extraIncList, azslcArgs, dxcArgs, az
     f = open(codeOut, "wb+")
     for incFile in extraIncList:
         fInFullname = os.path.join(os.path.dirname(thefile), incFile)
-        if not os.path.exists(fInFullname): 
+        if not os.path.exists(fInFullname):
             if not silent: print (fg.RED+ style.BRIGHT+ "Include file {} doesn't exist!".format(fInFullname)+ style.RESET_ALL)
             return buildFailed
         fIn = open(fInFullname, "rb")
@@ -142,7 +142,7 @@ def buildDXC(thefile, compilerPath, silent, extraIncList, azslcArgs, dxcArgs, az
         fIn.close()
     f.write(stdout)
     f.close()
-        
+
     dxcCompileArgs = [dxcPath, "-T", "vs_6_2", "-E", "MainVS"] + dxcArgs + ["-Fo", sbinVS, codeOut]
     process = subprocess.Popen(dxcCompileArgs, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
     out, err = process.communicate()
@@ -150,7 +150,7 @@ def buildDXC(thefile, compilerPath, silent, extraIncList, azslcArgs, dxcArgs, az
     if not os.path.exists(sbinVS):
         if not silent: print (fg.RED + style.BRIGHT + "Failed to compile MainVS with dxc.exe(for "+OutFormat+")." + style.RESET_ALL)
         return buildFailed
-    
+
     dxcCompileArgs = [dxcPath, "-T", "ps_6_2", "-E", "MainPS"] + dxcArgs + ["-Fo", sbinPS, codeOut]
     process = subprocess.Popen(dxcCompileArgs, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
     out, err = process.communicate()
@@ -172,12 +172,12 @@ def buildDXCCompute(thefile, compilerPath, silent, extraIncList, azslcArgs, dxcA
     inFile, inFileExt = os.path.splitext(thefile)
     codeOut  = inFile + ".hlsl"
     sbinCS   = inFile + "CS." + OutFormat
-    
+
     if os.path.exists(codeOut): os.remove(codeOut)
     if os.path.exists(sbinCS):  os.remove(sbinCS)
 
     stdout, ok = testfuncs.buildAndGet(thefile, compilerPath, silent, azslcArgs)
-    if not ok: 
+    if not ok:
         if not silent: print (fg.RED+ style.BRIGHT+ "Failed to generate .hlsl file with AZSLc."+ style.RESET_ALL)
         return buildFailed
 
@@ -185,7 +185,7 @@ def buildDXCCompute(thefile, compilerPath, silent, extraIncList, azslcArgs, dxcA
     f = open(codeOut, "wb+")
     for incFile in extraIncList:
         fInFullname = os.path.join(os.path.dirname(thefile), incFile)
-        if not os.path.exists(fInFullname): 
+        if not os.path.exists(fInFullname):
             if not silent: print (fg.RED+ style.BRIGHT+ "Include file {} doesn't exist!".format(fInFullname)+ style.RESET_ALL)
             return buildFailed
         fIn = io.open(fInFullname, "r", encoding="latin-1")
@@ -201,6 +201,6 @@ def buildDXCCompute(thefile, compilerPath, silent, extraIncList, azslcArgs, dxcA
     if not os.path.exists(sbinCS):
         if not silent: print (fg.RED+ style.BRIGHT+ "Failed to compile MainCS with dxc.exe."+ style.RESET_ALL)
         return buildFailed
-    
+
     if not silent: print (fg.GREEN+style.BRIGHT+ "["+thefile+"."+ OutFormat + "]" + style.RESET_ALL)
     return buildSucceeded

+ 8 - 1
tests/testapp.py

@@ -23,7 +23,7 @@ class testResult:
         self.numPass = p
         self.numTodo = t
         self.numFail = f
-        self.numEC = m
+        self.numEC = m   #EC = error code
 
     def add(self, other):
         self.numPass += other.numPass
@@ -186,6 +186,13 @@ def runAll(testsRoot, paths, compiler, verboseLevel, az3rdParty):
 if __name__ == "__main__":
     os.system('') # activate VT100 mode for windows console
 
+    try:
+        import yaml
+    except ImportError as err:
+        print ( fg.YELLOW + style.BRIGHT + "It seems your python environment lacks pyyaml. Run first through project-root's \"test.and.py\" (or pip install it)" + style.RESET_ALL )
+        if input("Continue (may result in false failures)? y/n:").lower() != "y":
+            exit(0)
+
     parser = ArgumentParser()
     parser.add_argument(
         '--path', dest='path',

+ 5 - 5
tests/testhelper.py

@@ -42,7 +42,7 @@ def found(needle, haystack):
         lastEnd = matchObject.end()
         return True
     return False
-    
+
 # pars the argument mentioned in the shader source file Ex : Cmdargs: --namespace=vk   or Cmdargs: ['--unique-idx', '--root-sig', '--root-const', '0']
 def parse_strlist(sl):
     clean = re.sub("[\[\],\s]","",sl)
@@ -89,7 +89,7 @@ def verifyEmissionPattern(azslFile, patternsFileName, compilerPath, silent, argL
         reset()
         ok = testfuncs.verifyAllPredicates(predicates, shaderCode, silent)
     return ok
-    
+
 # read the output of the compiler and tries to match some regex on it
 # to verify that the output looks like what we expect.
 def verifyEmissionPatterns(thefile, compilerPath, silent, argList):
@@ -104,19 +104,19 @@ def verifyEmissionPatterns(thefile, compilerPath, silent, argList):
         patterFileName = file
         if not verifyEmissionPattern(thefile, patterFileName, compilerPath, silent, argList):
             localFailList.append(patterFileName)
-        else: 
+        else:
             result = result + 1
     if len(localFailList) > 0:
         failList.extend(localFailList)
         return 0
     else:
         return result
-        
+
 def printFailedTestList(silent):
     global failList
     if not silent and len(failList) > 0:
         print(style.BRIGHT + fg.RED + "failed files: " + fg.WHITE + str(failList) + style.RESET_ALL)
-        
+
     failList = [] # since the module is imported for other platforms too, reset the list
 
 def compileAndExpectError(thefile, compilerPath, silent, argList):