summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--source/slang/slang-ast-support-types.h8
-rw-r--r--source/slang/slang-check-decl.cpp389
-rw-r--r--source/slang/slang-check-impl.h29
-rw-r--r--source/slang/slang-lookup.cpp15
-rw-r--r--source/slang/slang-lookup.h3
-rw-r--r--tests/language-feature/inheritance/struct-inherit-interface-requirement.slang69
-rw-r--r--tests/language-feature/inheritance/struct-inherit-interface-requirement.slang.expected.txt0
-rw-r--r--tests/language-feature/inheritance/struct-inheritance.slang58
-rw-r--r--tests/language-feature/inheritance/struct-inheritance.slang.expected.txt4
9 files changed, 560 insertions, 15 deletions
diff --git a/source/slang/slang-ast-support-types.h b/source/slang/slang-ast-support-types.h
index ef6589ca4..66144eaed 100644
--- a/source/slang/slang-ast-support-types.h
+++ b/source/slang/slang-ast-support-types.h
@@ -1016,6 +1016,13 @@ namespace Slang
Default = type | Function | Value,
};
+ /// Flags for options to be used when looking up declarations
+ enum class LookupOptions : uint8_t
+ {
+ None = 0,
+ IgnoreBaseInterfaces = 1 << 0,
+ };
+
// Represents one item found during lookup
struct LookupResultItem
{
@@ -1224,6 +1231,7 @@ namespace Slang
RefPtr<Scope> endScope = nullptr;
LookupMask mask = LookupMask::Default;
+ LookupOptions options = LookupOptions::None;
};
struct WitnessTable;
diff --git a/source/slang/slang-check-decl.cpp b/source/slang/slang-check-decl.cpp
index ab22108b9..ed273e2a4 100644
--- a/source/slang/slang-check-decl.cpp
+++ b/source/slang/slang-check-decl.cpp
@@ -1443,6 +1443,325 @@ namespace Slang
return false;
}
+ bool SemanticsVisitor::trySynthesizeMethodRequirementWitness(
+ ConformanceCheckingContext* context,
+ LookupResult const& lookupResult,
+ DeclRef<FuncDecl> requiredMemberDeclRef,
+ RefPtr<WitnessTable> witnessTable)
+ {
+ // The situation here is that the context of an inheritance
+ // declaration didn't provide an exact match for a required
+ // method. E.g.:
+ //
+ // interface ICounter { [mutating] int increment(); }
+ // struct MyCounter : ICounter
+ // {
+ // int increment(int val = 1) { ... }
+ // }
+ //
+ // It is clear in this case that the `MyCounter` type *can*
+ // satisfy the signature required by `ICounter`, but it has
+ // no explicit method declaration that is a perfect match.
+ //
+ // The approach in this function will be to construct a
+ // synthesized method along the lines of:
+ //
+ // struct MyCounter ...
+ // {
+ // ...
+ // int synthesized()
+ // {
+ // return this.increment();
+ // }
+ // }
+ //
+ // That is, we construct a method with the exact signature
+ // of the requirement (same parameter and result types),
+ // and then provide it with a body that simple `return`s
+ // the result of applying the desired requirement name
+ // (`increment` in this case) to those parameters.
+ //
+ // If the synthesized method type-checks, then we can say
+ // that the type must satisfy the requirement structurally,
+ // even if there isn't an exact signature match. More
+ // importantly, the method we just synthesized can be
+ // used as a witness to the fact that the requirement is
+ // satisfied.
+
+ // With the big picture spelled out, we can settle into
+ // the work of constructing our synthesized method.
+ //
+ auto synFuncDecl = m_astBuilder->create<FuncDecl>();
+
+ // For now our synthesized method will use the name and source
+ // location of the requirement we are trying to satisfy.
+ //
+ // TODO: as it stands right now our syntesized method will
+ // get a mangled name, which we don't actually want. Leaving
+ // out the name here doesn't help matters, because then *all*
+ // snthesized methods on a given type would share the same
+ // mangled name!
+ //
+ synFuncDecl->nameAndLoc = requiredMemberDeclRef.getDecl()->nameAndLoc;
+
+ // The result type of our synthesized method will be the expected
+ // result type from the interface requirement.
+ //
+ // TODO: This logic can/will run into problems if the return type
+ // is an associated type.
+ //
+ // The ideal solution is that we should be solving for interface
+ // conformance in two phases: a first phase to solve for how
+ // associated types are satisfied, and then a second phase to solve
+ // for how other requirements are satisfied (where we can substitute
+ // in the associated type witnesses for the abstract associated
+ // types as part of `requiredMemberDeclRef`).
+ //
+ // TODO: We should also double-check that this logic will work
+ // with a method that returns `This`.
+ //
+ auto resultType = getResultType(m_astBuilder, requiredMemberDeclRef);
+ synFuncDecl->returnType.type = resultType;
+
+ // Our synthesized method will have parameters matching the names
+ // and types of those on the requirement, and it will use expressions
+ // that reference those parametesr as arguments for the call expresison
+ // that makes up the body.
+ //
+ List<Expr*> synArgs;
+ for( auto paramDeclRef : getParameters(requiredMemberDeclRef) )
+ {
+ auto paramType = getType(m_astBuilder, paramDeclRef);
+
+ // For each parameter of the requirement, we create a matching
+ // parameter (same name and type) for the synthesized method.
+ //
+ auto synParamDecl = m_astBuilder->create<ParamDecl>();
+ synParamDecl->nameAndLoc = paramDeclRef.getDecl()->nameAndLoc;
+ synParamDecl->type.type = resultType;
+
+ // We need to add the parameter as a child declaration of
+ // the method we are building.
+ //
+ synParamDecl->parentDecl = synFuncDecl;
+ synFuncDecl->members.add(synParamDecl);
+
+ // For each paramter, we will create an argument expression
+ // for the call in the function body.
+ //
+ auto synArg = m_astBuilder->create<VarExpr>();
+ synArg->declRef = makeDeclRef(synParamDecl);
+ synArg->type = paramType;
+ synArgs.add(synArg);
+ }
+
+ // Required interface methods can be `static` or non-`static`,
+ // and non-`static` methods can be `[mutating]` or non-`[mutating]`.
+ // All of these details affect how we introduce our `this` parameter,
+ // if any.
+ //
+ ThisExpr* synThis = nullptr;
+ if( !requiredMemberDeclRef.getDecl()->hasModifier<HLSLStaticModifier>() )
+ {
+ // For a non-`static` requirement, we need a `this` parameter.
+ //
+ synThis = m_astBuilder->create<ThisExpr>();
+
+ // The type of `this` in our method will be the type for
+ // which we are synthesizing a conformance.
+ //
+ synThis->type.type = context->conformingType;
+
+ if( requiredMemberDeclRef.getDecl()->hasModifier<MutatingAttribute>() )
+ {
+ // If the interface requirement is `[mutating]` then our
+ // synthesized method should be too, and also the `this`
+ // parameter should be an l-value.
+ //
+ synThis->type.isLeftValue = true;
+
+ auto synMutatingAttr = m_astBuilder->create<MutatingAttribute>();
+ synFuncDecl->modifiers.first = synMutatingAttr;
+ }
+ }
+
+ // The body of our synthesized method is going to try to
+ // make a call using the name of the method requirement (e.g.,
+ // the name `increment` in our example at the top of this function).
+ //
+ // The caller already passed in a `LookupResult` that represents
+ // an attempt to look up the given name in the type of `this`,
+ // and we really just need to wrap that result up as an overloaded
+ // expression.
+ //
+ auto synBase = m_astBuilder->create<OverloadedExpr>();
+ synBase->lookupResult2 = lookupResult;
+
+ // If `synThis` is non-null, then we will use it as the base of
+ // the overloaded expression, so that we have an overloaded
+ // member reference, and not just an overloaded reference to some
+ // static definitions.
+ //
+ synBase->base = synThis;
+
+ // We now have the reference to the overload group we plan to call,
+ // and we already built up the argument list, so we can construct
+ // an `InvokeExpr` that represents the call we want to make.
+ //
+ auto synCall = m_astBuilder->create<InvokeExpr>();
+ synCall->functionExpr = synBase;
+ synCall->arguments = synArgs;
+
+ // In order to know if our call is well-formed, we need to run
+ // the semantic checking logic for overload resolution. If it
+ // runs into an error, we don't want that being reported back
+ // to the user as some kind of overload-resolution failure.
+ //
+ // In order to protect the user from whatever errors might
+ // occur, we will swap out the current diagnostic sink for
+ // a temporary one.
+ //
+ DiagnosticSink* savedSink = m_shared->m_sink;
+ DiagnosticSink tempSink(savedSink->getSourceManager());
+ m_shared->m_sink = &tempSink;
+
+ // With our temporary diagnostic sink soaking up any messages
+ // from overload resolution, we can now try to resolve
+ // the call to see what happens.
+ //
+ auto checkedCall = ResolveInvoke(synCall);
+
+ // Of course, it is possible that the call went through fine,
+ // but the result isn't of the type we expect/require,
+ // so we also need to coerce the result of the call to
+ // the expected type.
+ //
+ auto coercedCall = coerce(resultType, checkedCall);
+
+ // Once we are done making our semantic checks, we can
+ // restore the original sink, so that subsequent operations
+ // report diagnostics as usual.
+ //
+ m_shared->m_sink = savedSink;
+
+ // If our overload resolution or type coercion failed,
+ // then we have not been able to synthesize a witness
+ // for the requirement.
+ //
+ // TODO: We might want to detect *why* overload resolution
+ // or type coercion failed, and report errors accordingly.
+ //
+ // More detailed diagnostics could help users understand
+ // what they did wrong, e.g.:
+ //
+ // * "We tried to use `foo(int)` but the interface requires `foo(String)`
+ //
+ // * "You have two methods that can apply as `bar()` and we couldn't tell which one you meant
+ //
+ // For now we just bail out here and rely on the caller to
+ // diagnose a generic "failed to satisfying requirement" error.
+ //
+ if(tempSink.getErrorCount() != 0)
+ return false;
+
+ // If we were able to type-check the call, then we should
+ // be able to finish construction of a suitable witness.
+ //
+ // We've already created the outer declaration (including its
+ // parameters), and the inner expression, so the main work
+ // that is left is defining the body of the new function,
+ // which comprises a single `return` statement.
+ //
+ auto synReturn = m_astBuilder->create<ReturnStmt>();
+ synReturn->expression = coercedCall;
+
+ synFuncDecl->body = synReturn;
+
+ // Once we are sure that we want to use the declaration
+ // we've synthesized, aew can go ahead and wire it up
+ // to the AST so that subsequent stages can generate
+ // IR code from it.
+ //
+ // Note: we set the parent of the synthesized declaration
+ // to the parent of the inheritance declaration being
+ // validated (which is either a type declaration or
+ // an `extension`), but we do *not* add the syntehsized
+ // declaration to the list of child declarations at
+ // this point.
+ //
+ // By leaving the synthesized declaration off of the list
+ // of members, we ensure that it doesn't get found
+ // by lookup (e.g., in a module that `import`s this type).
+ // Unfortunately, we may also break invariants in other parts
+ // of the code if they assume that all declarations have
+ // to appear in the parent/child hierarchy of the module.
+ //
+ // TODO: We may need to properly wire the synthesized
+ // declaration into the hierarchy, but then attach a modifier
+ // to it to indicate that it should be ignored by things like lookup.
+ //
+ synFuncDecl->parentDecl = context->parentDecl;
+
+ // Once our synthesized declaration is complete, we need
+ // to install it as the witness that satifies the given
+ // requirement.
+ //
+ // Subsequent code generation should not be able to tell the
+ // difference between our synthetic method and a hand-written
+ // one with the same behavior.
+ //
+ witnessTable->requirementDictionary.Add(requiredMemberDeclRef,
+ RequirementWitness(makeDeclRef(synFuncDecl)));
+ return true;
+ }
+
+ bool SemanticsVisitor::trySynthesizeRequirementWitness(
+ ConformanceCheckingContext* context,
+ LookupResult const& lookupResult,
+ DeclRef<Decl> requiredMemberDeclRef,
+ RefPtr<WitnessTable> witnessTable)
+ {
+ SLANG_UNUSED(lookupResult);
+ SLANG_UNUSED(requiredMemberDeclRef);
+ SLANG_UNUSED(witnessTable);
+
+ if (auto requiredFuncDeclRef = requiredMemberDeclRef.as<FuncDecl>())
+ {
+ // Check signature match.
+ return trySynthesizeMethodRequirementWitness(
+ context,
+ lookupResult,
+ requiredFuncDeclRef,
+ witnessTable);
+ }
+
+ // TODO: There are other kinds of requirements for which synthesis should
+ // be possible:
+ //
+ // * It should be possible to synthesize required initializers
+ // using an approach similar to what is used for methods.
+ //
+ // * We should be able to synthesize subscripts with different
+ // signatures (taking into account default parameters) and even
+ // different accessors (e.g., synthesizing the `get` and `set`
+ // accessors from a `ref` accessor)
+ //
+ // * When we support property declarations, it should be possible
+ // to synthesize a property requirement using a field of the
+ // same name.
+ //
+ // * For specific kinds of generic requirements, we should be able
+ // to wrap the synthesis of the inner declaration in synthesis
+ // of an outer generic with a matching signature.
+ //
+ // All of these cases can/should use similar logic to
+ // `trySynthesizeMethodRequirementWitness` where they construct an AST
+ // in the form of what the use site ought to look like, and then
+ // apply existing semantic checking logic to generate the code.
+
+ return false;
+ }
+
bool SemanticsVisitor::findWitnessForInterfaceRequirement(
ConformanceCheckingContext* context,
Type* type,
@@ -1525,29 +1844,72 @@ namespace Slang
// but would require synthesizing proxy/forwarding
// implementations in the type itself.
//
- // We will punt on the second issue for now (since
- // transparent members aren't currently exposed as
- // a general-purpose feature for users), and rely
- // on subsequent checking in this function to
- // rule out inherited abstract members.
+ // For the first issue, we will use a flag to influence
+ // lookup so that it doesn't include results looked up
+ // through interface inheritance clauses (but it *will*
+ // look up result through inheritance clauses corresponding
+ // to concrete types).
//
- auto lookupResult = lookUpMember(m_astBuilder, this, name, type);
+ // The second issue of members that require us to proxy/forward
+ // requests will be handled further down. For now we include
+ // lookup results that might be usable, but not as-is.
+ //
+ auto lookupResult = lookUpMember(m_astBuilder, this, name, type, LookupMask::Default, LookupOptions::IgnoreBaseInterfaces);
+
+ if(!lookupResult.isValid())
+ {
+ // If we failed to even look up a member with the name of the
+ // requirement, then we can be certain that the type doesn't
+ // satisfy the requirement.
+ //
+ // TODO: If we ever allowed certain kinds of requirements to
+ // be inferred (e.g., inferring associated types from the
+ // signatures of methods, as is done for Swift), we'd
+ // need to revisit this step.
+ //
+ getSink()->diagnose(inheritanceDecl, Diagnostics::typeDoesntImplementInterfaceRequirement, type, requiredMemberDeclRef);
+ return false;
+ }
// Iterate over the members and look for one that matches
// the expected signature for the requirement.
for (auto member : lookupResult)
{
+ // To a first approximation, any lookup result that required a "breadcrumb"
+ // will not be usable to directly satisfy an interface requirement, since
+ // each breadcrumb will amount to a manipulation of `this` that is required
+ // to make the declaration usable (e.g., casting to a base type).
+ //
+ if(member.breadcrumbs != nullptr)
+ continue;
+
if (doesMemberSatisfyRequirement(member.declRef, requiredMemberDeclRef, witnessTable))
return true;
}
- // No suitable member found, although there were candidates.
+ // If we reach this point then there were no members suitable
+ // for satisfying the interface requirement *diretly*.
+ //
+ // It is possible that one of the items in `lookupResult` could be
+ // used to synthesize an exact-match witness, by generating the
+ // code required to handle all the conversions that might be
+ // required on `this`.
+ //
+ if( trySynthesizeRequirementWitness(context, lookupResult, requiredMemberDeclRef, witnessTable) )
+ {
+ return true;
+ }
+
+ // We failed to find a member of the type that can be used
+ // to satisfy the requirement (even via synthesis), so we
+ // need to report the failure to the user.
//
// TODO: Eventually we might want something akin to the current
// overload resolution logic, where we keep track of a list
// of "candidates" for satisfaction of the requirement,
- // and if nothing is found we print the candidates
-
+ // and if nothing is found we print the candidates that made it
+ // furthest in checking.
+ //
getSink()->diagnose(inheritanceDecl, Diagnostics::typeDoesntImplementInterfaceRequirement, type, requiredMemberDeclRef);
return false;
}
@@ -1701,7 +2063,8 @@ namespace Slang
bool SemanticsVisitor::checkConformance(
Type* type,
- InheritanceDecl* inheritanceDecl)
+ InheritanceDecl* inheritanceDecl,
+ ContainerDecl* parentDecl)
{
if( auto declRefType = as<DeclRefType>(type) )
{
@@ -1736,6 +2099,8 @@ namespace Slang
auto baseType = inheritanceDecl->base.type;
ConformanceCheckingContext context;
+ context.conformingType = type;
+ context.parentDecl = parentDecl;
RefPtr<WitnessTable> witnessTable = checkConformanceToType(&context, type, inheritanceDecl, baseType);
if(!witnessTable)
return false;
@@ -1751,7 +2116,7 @@ namespace Slang
for (auto inheritanceDecl : decl->getMembersOfType<InheritanceDecl>())
{
- checkConformance(targetType, inheritanceDecl);
+ checkConformance(targetType, inheritanceDecl, decl);
}
}
@@ -1789,7 +2154,7 @@ namespace Slang
// (That's what C# does).
for (auto inheritanceDecl : decl->getMembersOfType<InheritanceDecl>())
{
- checkConformance(type, inheritanceDecl);
+ checkConformance(type, inheritanceDecl, decl);
}
}
}
diff --git a/source/slang/slang-check-impl.h b/source/slang/slang-check-impl.h
index 93ae87f40..931e331a6 100644
--- a/source/slang/slang-check-impl.h
+++ b/source/slang/slang-check-impl.h
@@ -737,9 +737,35 @@ namespace Slang
//
struct ConformanceCheckingContext
{
+ /// The type for which conformances are being checked
+ Type* conformingType;
+
+ /// The outer declaration for the conformances being checked (either a type or `extension` declaration)
+ ContainerDecl* parentDecl;
+
Dictionary<DeclRef<InterfaceDecl>, RefPtr<WitnessTable>> mapInterfaceToWitnessTable;
};
+ /// Attempt to synthesize a method that can satisfy `requiredMemberDeclRef` using `lookupResult`.
+ ///
+ /// On success, installs the syntethesized method in `witnessTable` and returns `true`.
+ /// Otherwise, returns `false`.
+ bool trySynthesizeMethodRequirementWitness(
+ ConformanceCheckingContext* context,
+ LookupResult const& lookupResult,
+ DeclRef<FuncDecl> requiredMemberDeclRef,
+ RefPtr<WitnessTable> witnessTable);
+
+ /// Attempt to synthesize a declartion that can satisfy `requiredMemberDeclRef` using `lookupResult`.
+ ///
+ /// On success, installs the syntethesized declaration in `witnessTable` and returns `true`.
+ /// Otherwise, returns `false`.
+ bool trySynthesizeRequirementWitness(
+ ConformanceCheckingContext* context,
+ LookupResult const& lookupResult,
+ DeclRef<Decl> requiredMemberDeclRef,
+ RefPtr<WitnessTable> witnessTable);
+
// Find the appropriate member of a declared type to
// satisfy a requirement of an interface the type
// claims to conform to.
@@ -782,7 +808,8 @@ namespace Slang
/// inheritance to be valid.
bool checkConformance(
Type* type,
- InheritanceDecl* inheritanceDecl);
+ InheritanceDecl* inheritanceDecl,
+ ContainerDecl* parentDecl);
void checkExtensionConformance(ExtensionDecl* decl);
diff --git a/source/slang/slang-lookup.cpp b/source/slang/slang-lookup.cpp
index 19cd4cafe..855497d00 100644
--- a/source/slang/slang-lookup.cpp
+++ b/source/slang/slang-lookup.cpp
@@ -527,6 +527,17 @@ static void _lookUpMembersInSuperTypeDeclImpl(
for (auto inheritanceDeclRef : getMembersOfType<InheritanceDecl>(aggTypeDeclBaseRef))
{
ensureDecl(semantics, inheritanceDeclRef.getDecl(), DeclCheckState::CanUseBaseOfInheritanceDecl);
+
+ auto baseType = getSup(astBuilder, inheritanceDeclRef);
+ if( auto baseDeclRefType = as<DeclRefType>(baseType) )
+ {
+ if( auto baseInterfaceDeclRef = baseDeclRefType->declRef.as<InterfaceDecl>() )
+ {
+ if( int(request.options) & int(LookupOptions::IgnoreBaseInterfaces) )
+ continue;
+ }
+ }
+
_lookUpMembersInSuperType(astBuilder, name, leafType, leafIsSuperWitness, inheritanceDeclRef, request, ioResult, inBreadcrumbs);
}
}
@@ -807,11 +818,13 @@ LookupResult lookUpMember(
SemanticsVisitor* semantics,
Name* name,
Type* type,
- LookupMask mask)
+ LookupMask mask,
+ LookupOptions options)
{
LookupRequest request;
request.semantics = semantics;
request.mask = mask;
+ request.options = options;
LookupResult result;
_lookUpMembersInType(astBuilder, name, type, request, result, nullptr);
diff --git a/source/slang/slang-lookup.h b/source/slang/slang-lookup.h
index 1402918ad..f983bfefc 100644
--- a/source/slang/slang-lookup.h
+++ b/source/slang/slang-lookup.h
@@ -30,7 +30,8 @@ LookupResult lookUpMember(
SemanticsVisitor* semantics,
Name* name,
Type* type,
- LookupMask mask = LookupMask::Default);
+ LookupMask mask = LookupMask::Default,
+ LookupOptions options = LookupOptions::None);
/// Perform "direct" lookup in a container declaration
LookupResult lookUpDirectAndTransparentMembers(
diff --git a/tests/language-feature/inheritance/struct-inherit-interface-requirement.slang b/tests/language-feature/inheritance/struct-inherit-interface-requirement.slang
new file mode 100644
index 000000000..e18695737
--- /dev/null
+++ b/tests/language-feature/inheritance/struct-inherit-interface-requirement.slang
@@ -0,0 +1,69 @@
+// struct-inherit-interface-requirement.slang
+
+//TEST(compute):COMPARE_COMPUTE:
+
+// Test that a `struct` type can use an inherited
+// member to satisfy an interface requirement.
+
+interface ITweak
+{
+ int tweak(int val);
+ int twiddle(int val);
+}
+
+// Note: `Base` intentionally doesn't inherit from `ITweak`,
+// but it *does* provide a method that could satisfy one
+// of the interface requirements.
+//
+struct Base
+{
+ int a;
+
+ int tweak(int val) { return val ^ a; }
+}
+
+struct Derived : Base, ITweak
+{
+ // Note: it is important for this type to have an additional
+ // field beyond the one in `Base`, because it ensures that
+ // the two types `Base` and `Derived` aren't structurally
+ // equivalent when compiled through HLSL (which silently allows
+ // certain type mismatches so long as there is a memberwise
+ // structural match).
+ int b;
+
+ int twiddle(int val)
+ {
+ return val + b;
+ }
+}
+
+int tweakAndTwiddle<T : ITweak>(T tweaker, int val)
+{
+ int tmp = val;
+ tmp = tweaker.tweak(val);
+ tmp = tweaker.twiddle(val);
+ return val;
+}
+
+
+int test(int val)
+{
+ Derived d;
+ d.a = 0xFF;
+ d.b = 1;
+
+ return tweakAndTwiddle(d, val);
+}
+
+//TEST_INPUT:ubuffer(data=[0 0 0 0], stride=4):out,name=outputBuffer
+RWStructuredBuffer<int> outputBuffer;
+
+[numthreads(4, 1, 1)]
+void computeMain(uint3 dispatchThreadID : SV_DispatchThreadID)
+{
+ uint tid = dispatchThreadID.x;
+ int inVal = tid;
+ int outVal = test(inVal);
+ outputBuffer[tid] = outVal;
+}
diff --git a/tests/language-feature/inheritance/struct-inherit-interface-requirement.slang.expected.txt b/tests/language-feature/inheritance/struct-inherit-interface-requirement.slang.expected.txt
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/language-feature/inheritance/struct-inherit-interface-requirement.slang.expected.txt
diff --git a/tests/language-feature/inheritance/struct-inheritance.slang b/tests/language-feature/inheritance/struct-inheritance.slang
new file mode 100644
index 000000000..e2cfaf25b
--- /dev/null
+++ b/tests/language-feature/inheritance/struct-inheritance.slang
@@ -0,0 +1,58 @@
+// struct-inheritance.slang
+
+//TEST(compute):COMPARE_COMPUTE:
+
+// Test that we can define a `struct` type
+// that inherits from another `struct`.
+
+struct Base
+{
+ int a;
+
+ int tweakBase(int val) { return val ^ a; }
+}
+
+struct Derived : Base
+{
+ int b;
+
+ int tweakDerived(int val) { return tweakBase(val) + b; }
+}
+
+int tweak(Base b, int v)
+{
+ return b.tweakBase(v);
+}
+
+//TEST_INPUT:cbuffer(data=[1 2]):name=C
+cbuffer C
+{
+ int x;
+ int y;
+}
+
+int test(int val)
+{
+ Derived d;// = { x, y };
+ d.a = x;
+ d.b = y;
+
+ int result = 0;
+ result = result*16 + d.a;
+ result = result*16 + d.tweakBase(val);
+ result = result*16 + tweak(d, val);
+ result = result*16 + d.tweakDerived(val);
+ return result;
+}
+
+//TEST_INPUT:ubuffer(data=[0 0 0 0], stride=4):out,name=outputBuffer
+RWStructuredBuffer<int> outputBuffer;
+
+[numthreads(4, 1, 1)]
+void computeMain(uint3 dispatchThreadID : SV_DispatchThreadID)
+{
+ uint tid = dispatchThreadID.x;
+ int inVal = tid;
+ int outVal = test(inVal);
+ outputBuffer[tid] = outVal;
+}
diff --git a/tests/language-feature/inheritance/struct-inheritance.slang.expected.txt b/tests/language-feature/inheritance/struct-inheritance.slang.expected.txt
new file mode 100644
index 000000000..a17826230
--- /dev/null
+++ b/tests/language-feature/inheritance/struct-inheritance.slang.expected.txt
@@ -0,0 +1,4 @@
+1113
+1002
+1335
+1224