Skip to content

Commit 583c72a

Browse files
author
Tim Foley
authored
First step toward supporting use of interfaces as existential types (shader-slang#716)
* First step toward supporting use of interfaces as existential types Traditional generics involve universal quantification. E.g., a declaration like: ``` void drive<T : IVehicle>(T vehicle); ``` indicates for *for all* types `T` that implement the `IVehicle` interface, the `drive()` function is available. In contrast, whend directly using an interface type like: ``` IVehicle v = ...; v.doSomething(); ``` we only know that there *exists* some concrete type (we could call it `E`) such that `v` refers to a value of type `E`, and `E` implements the `IVehicle` interface. In order to perform an operation like `v.doSomething()` we need to "open" the existential value so that we can look at the concrete type and how it implements the `IVehicle.doSomething` requirement. This change adds a very explicit representation of existentials to Slang's IR. An operation like `e = makeExistential(v, w)` creates a value of some existential type (interfaces being our only existential types for now), by wrapping a concrete value `v` (the type of `v` can be seen as an implicit operand) and a witness table `w` showing that the type of `v` implements the requirements of the chosen interface type. In turn, opening of an existential is handled with operations `extractExistential{Value|Type|WitnessTable}` which pull the corresponding piece of information out of a value of existential type (which somewhere in the code had to have been created with `makeExistential`). The change includes a trivial simplification pass that can detect cases where an `extractExistential*` operation is applied direclty to a `makeExistential` operation, so that there is only one possible result that could be extracted. This allows for simplification of existential types used in trivial ways for local variables (this is mostly so I can check in a functional test, rather than to actually support useful code involving interfaces right now). The logic in the semantic checking phase of the compiler is comparatively more complex. When we are about to perform member lookup given an expression like `obj.member` we will first check if `obj` has an existential type, and if it does we will construct a suitable local context in which we extract the value, type, and witness table from the existential (these all become explicit AST expression nodes), and then use the extracted value as the base of the lookup operation. The nature of existential values is that two different values with the same existential (interface) type could wrap concrete values with differnt types, so that we need to carefully refer only to the extracted type/value/witness-table of specific *values*. We handle this right now by conceptually moving the existential-type value into a local variable (by introducing a `LetExpr` that amounts to `let v = <init> in <body>`) and then require that the extract expressions must refer to the (immutable) variable declaration from which they are extracting a value. (Eventually we should expand this so that when using an immutable local variable of existential type we just use that variable as-is rather than introduce a new temporary) A simple test case is included that uses an interface type in an almost trivial way for a local variable; this test can be run and produces the expected results. A more complex test case that passes an existential into a function is included, but left disabled because a more aggressive simplification approach is required to generate working code from it. * Add missing file for expected test output * Fixups for merge from top-of-tree
1 parent 3a02c59 commit 583c72a

19 files changed

+753
-38
lines changed

source/slang/check.cpp

+135-29
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,80 @@ namespace Slang
561561
return isDeclUsableAsStaticMember(decl);
562562
}
563563

564+
RefPtr<Expr> maybeOpenExistential(RefPtr<Expr> expr)
565+
{
566+
auto exprType = expr->type.type;
567+
568+
if(auto declRefType = exprType->As<DeclRefType>())
569+
{
570+
if(auto interfaceDeclRef = declRefType->declRef.As<InterfaceDecl>())
571+
{
572+
// Is there an this-type substitution being applied, so that
573+
// we are referencing the interface type through a concrete
574+
// type (e.g., a type parameter constrainted to this interface)?
575+
//
576+
// Because of the way that substitutions need to mirror the nesting
577+
// hierarchy of declarations, any this-type substitution pertaining
578+
// to the chosen interface decl must be the first substitution on
579+
// the list (which is a linked list from the "inside" out).
580+
//
581+
auto thisTypeSubst = interfaceDeclRef.substitutions.substitutions.As<ThisTypeSubstitution>();
582+
if(thisTypeSubst && thisTypeSubst->interfaceDecl == interfaceDeclRef.decl)
583+
{
584+
// This isn't really an existential type, because somebody
585+
// has already filled in a this-type substitution.
586+
}
587+
else
588+
{
589+
// Okay, here is the case that matters.
590+
//
591+
592+
auto interfaceDecl = interfaceDeclRef.getDecl();
593+
594+
RefPtr<Variable> varDecl = new Variable();
595+
varDecl->ParentDecl = nullptr; // TODO: need to fill this in somehow!
596+
varDecl->checkState = DeclCheckState::Checked;
597+
varDecl->nameAndLoc.loc = expr->loc;
598+
varDecl->initExpr = expr;
599+
varDecl->type.type = expr->type.type;
600+
601+
auto varDeclRef = makeDeclRef(varDecl.Ptr());
602+
603+
RefPtr<LetExpr> letExpr = new LetExpr();
604+
letExpr->decl = varDecl;
605+
606+
RefPtr<ExtractExistentialType> openedType = new ExtractExistentialType();
607+
openedType->declRef = varDeclRef;
608+
609+
RefPtr<ExtractExistentialSubtypeWitness> openedWitness = new ExtractExistentialSubtypeWitness();
610+
openedWitness->sub = openedType;
611+
openedWitness->sup = expr->type.type;
612+
openedWitness->declRef = varDeclRef;
613+
614+
RefPtr<ThisTypeSubstitution> openedThisType = new ThisTypeSubstitution();
615+
openedThisType->outer = interfaceDeclRef.substitutions.substitutions;
616+
openedThisType->interfaceDecl = interfaceDecl;
617+
openedThisType->witness = openedWitness;
618+
619+
DeclRef<InterfaceDecl> substDeclRef = DeclRef<InterfaceDecl>(interfaceDecl, openedThisType);
620+
auto substDeclRefType = DeclRefType::Create(getSession(), substDeclRef);
621+
622+
RefPtr<ExtractExistentialValueExpr> openedValue = new ExtractExistentialValueExpr();
623+
openedValue->declRef = varDeclRef;
624+
openedValue->type = QualType(substDeclRefType);
625+
626+
letExpr->body = openedValue;
627+
letExpr->type = openedValue->type;
628+
629+
return letExpr;
630+
}
631+
}
632+
}
633+
634+
// Default: apply the callback to the original expression;
635+
return expr;
636+
}
637+
564638
RefPtr<Expr> ConstructDeclRefExpr(
565639
DeclRef<Decl> declRef,
566640
RefPtr<Expr> baseExpr,
@@ -577,6 +651,15 @@ namespace Slang
577651
{
578652
// If there was a base expression, we will have some kind of
579653
// member expression.
654+
655+
// We want to check for the case where the base "expression"
656+
// actually names a type, because in that case we are doing
657+
// a static member reference.
658+
//
659+
// TODO: Should we be checking if the member is static here?
660+
// If it isn't, should we be automatically producing a "curried"
661+
// form (e.g., for a member function, return a value usable
662+
// for referencing it as a free function).
580663
//
581664
if (baseExpr->type->As<TypeType>())
582665
{
@@ -1440,15 +1523,22 @@ namespace Slang
14401523
// Trying to convert to an interface type.
14411524
//
14421525
// We will allow this if the type conforms to the interface.
1443-
if (DoesTypeConformToInterface(fromType, interfaceDeclRef))
1526+
1527+
if(auto witness = tryGetInterfaceConformanceWitness(fromType, interfaceDeclRef))
14441528
{
14451529
if (outToExpr)
1446-
*outToExpr = CreateImplicitCastExpr(toType, fromExpr);
1530+
*outToExpr = createCastToInterfaceExpr(toType, fromExpr, witness);
14471531
if (outCost)
14481532
*outCost = kConversionCost_CastToInterface;
14491533
return true;
14501534
}
14511535
}
1536+
1537+
// Note: The following seems completely broken, and we should be using
1538+
// a `fromTypeDeclRef` here for the case when casting *from* a generic
1539+
// type parameter to an interface type...
1540+
//
1541+
#if 0
14521542
else if (auto genParamDeclRef = toTypeDeclRef.As<GenericTypeParamDecl>())
14531543
{
14541544
// We need to enumerate the constraints placed on this type by its outer
@@ -1483,6 +1573,7 @@ namespace Slang
14831573
}
14841574

14851575
}
1576+
#endif
14861577

14871578
}
14881579

@@ -1721,6 +1812,25 @@ namespace Slang
17211812
return castExpr;
17221813
}
17231814

1815+
/// Create an "up-cast" from a value to an interface type
1816+
///
1817+
/// This operation logically constructs an "existential" value,
1818+
/// which packages up the value, its type, and the witness
1819+
/// of its conformance to the interface.
1820+
///
1821+
RefPtr<Expr> createCastToInterfaceExpr(
1822+
RefPtr<Type> toType,
1823+
RefPtr<Expr> fromExpr,
1824+
RefPtr<Val> witness)
1825+
{
1826+
RefPtr<CastToInterfaceExpr> expr = new CastToInterfaceExpr();
1827+
expr->loc = fromExpr->loc;
1828+
expr->type = QualType(toType);
1829+
expr->valueArg = fromExpr;
1830+
expr->witnessArg = witness;
1831+
return expr;
1832+
}
1833+
17241834
// Perform type coercion, and emit errors if it isn't possible
17251835
RefPtr<Expr> Coerce(
17261836
RefPtr<Type> toType,
@@ -7900,36 +8010,24 @@ namespace Slang
79008010
// deal with this cases here, even if they are no-ops.
79018011
//
79028012

7903-
RefPtr<Expr> visitDerefExpr(DerefExpr* expr)
7904-
{
7905-
SLANG_DIAGNOSE_UNEXPECTED(getSink(), expr, "should not appear in input syntax");
7906-
return expr;
8013+
#define CASE(NAME) \
8014+
RefPtr<Expr> visit##NAME(NAME* expr) \
8015+
{ \
8016+
SLANG_DIAGNOSE_UNEXPECTED(getSink(), expr, \
8017+
"should not appear in input syntax"); \
8018+
return expr; \
79078019
}
79088020

7909-
RefPtr<Expr> visitSwizzleExpr(SwizzleExpr* expr)
7910-
{
7911-
SLANG_DIAGNOSE_UNEXPECTED(getSink(), expr, "should not appear in input syntax");
7912-
return expr;
7913-
}
8021+
CASE(DerefExpr)
8022+
CASE(SwizzleExpr)
8023+
CASE(OverloadedExpr)
8024+
CASE(OverloadedExpr2)
8025+
CASE(AggTypeCtorExpr)
8026+
CASE(CastToInterfaceExpr)
8027+
CASE(LetExpr)
8028+
CASE(ExtractExistentialValueExpr)
79148029

7915-
RefPtr<Expr> visitOverloadedExpr(OverloadedExpr* expr)
7916-
{
7917-
SLANG_DIAGNOSE_UNEXPECTED(getSink(), expr, "should not appear in input syntax");
7918-
return expr;
7919-
}
7920-
7921-
RefPtr<Expr> visitOverloadedExpr2(OverloadedExpr2* expr)
7922-
{
7923-
SLANG_DIAGNOSE_UNEXPECTED(getSink(), expr, "should not appear in input syntax");
7924-
return expr;
7925-
}
7926-
7927-
7928-
RefPtr<Expr> visitAggTypeCtorExpr(AggTypeCtorExpr* expr)
7929-
{
7930-
SLANG_DIAGNOSE_UNEXPECTED(getSink(), expr, "should not appear in input syntax");
7931-
return expr;
7932-
}
8030+
#undef CASE
79338031

79348032
//
79358033
//
@@ -8091,6 +8189,14 @@ namespace Slang
80918189

80928190
expr->BaseExpression = MaybeDereference(expr->BaseExpression);
80938191

8192+
// If the base of the member lookup has an interface type
8193+
// *without* a suitable this-type substitution, then we are
8194+
// trying to perform lookup on a value of existential type,
8195+
// and we should "open" the existential here so that we
8196+
// can expose its structure.
8197+
//
8198+
expr->BaseExpression = maybeOpenExistential(expr->BaseExpression);
8199+
80948200
auto & baseType = expr->BaseExpression->type;
80958201

80968202
// Note: Checking for vector types before declaration-reference types,

source/slang/emit.cpp

+23
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include "../core/slang-writer.h"
55
#include "ir-dce.h"
6+
#include "ir-existential.h"
67
#include "ir-insts.h"
78
#include "ir-restructure.h"
89
#include "ir-restructure-scoping.h"
@@ -6532,6 +6533,26 @@ String emitEntryPoint(
65326533
// un-specialized IR.
65336534
dumpIRIfEnabled(compileRequest, irModule);
65346535

6536+
// Any code that makes use of existential (interface) types
6537+
// needs to be simplified to use concrete types instead,
6538+
// wherever this is possible.
6539+
//
6540+
// Note: we are applying this *before* doing specialization
6541+
// of generics because this pass could expose concrete
6542+
// types and/or witness tables that allow for further
6543+
// specialization.
6544+
//
6545+
// TODO: Simplification of existential-based and generics-based
6546+
// code may each open up opportunities for the other, so
6547+
// in the long run these will need to be merged into a
6548+
// single pass that looks for all simplification opportunities.
6549+
//
6550+
// TODO: We also need a legalization pass that will "expose"
6551+
// existential values that are nested inside of other types,
6552+
// so that the simplifications can be applied.
6553+
//
6554+
simplifyExistentialTypes(irModule);
6555+
65356556
// Next, we need to ensure that the code we emit for
65366557
// the target doesn't contain any operations that would
65376558
// be illegal on the target platform. For example,
@@ -6540,6 +6561,8 @@ String emitEntryPoint(
65406561
//
65416562
specializeGenerics(irModule, sharedContext.target);
65426563

6564+
6565+
65436566
// Debugging code for IR transformations...
65446567
#if 0
65456568
dumpIRIfEnabled(compileRequest, irModule, "SPECIALIZED");

source/slang/expr-defs.h

+24
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,17 @@ END_SYNTAX_CLASS()
137137

138138
// An implicit type-cast inserted during semantic checking
139139
SYNTAX_CLASS(ImplicitCastExpr, TypeCastExpr)
140+
END_SYNTAX_CLASS()
141+
142+
/// A cast from a value to an interface ("existential") type.
143+
SYNTAX_CLASS(CastToInterfaceExpr, Expr)
144+
RAW(
145+
/// The value being cast to an interface type
146+
RefPtr<Expr> valueArg;
147+
148+
/// A witness showing that `valueArg` conforms to the chosen interface
149+
RefPtr<Val> witnessArg;
150+
)
140151
END_SYNTAX_CLASS()
141152

142153
SIMPLE_SYNTAX_CLASS(SelectExpr, OperatorExpr)
@@ -169,3 +180,16 @@ SYNTAX_CLASS(ThisExpr, Expr)
169180
FIELD(RefPtr<Scope>, scope);
170181
END_SYNTAX_CLASS()
171182

183+
// An expression that binds a temporary variable in a local expression context
184+
SYNTAX_CLASS(LetExpr, Expr)
185+
RAW(
186+
RefPtr<VarDeclBase> decl;
187+
RefPtr<Expr> body;
188+
)
189+
END_SYNTAX_CLASS()
190+
191+
SYNTAX_CLASS(ExtractExistentialValueExpr, Expr)
192+
RAW(
193+
DeclRef<VarDeclBase> declRef;
194+
)
195+
END_SYNTAX_CLASS()

0 commit comments

Comments
 (0)