Skip to content

Commit dcd9f78

Browse files
author
Tim Foley
authored
Add basic support for [mutating] methods (shader-slang#667)
By default, when writing a "method" (aka "member function") in Slang, the `this` parameter is implicitly an `in` parameter. So this: ```hlsl struct Foo { int state; int getState() { return state; } void setState(int s) { state = s; } }; ``` is desugared into something like this: ```hlsl struct Foo { int state }; int Foo_getState(Foo this) { return this.state; } // BAD: void Foo_setState(Foo this, int s) { this.state = s; } ``` That "setter" doesn't really do what was intended. It modifies a local copy of type `Foo`, because `in` parameters in HLSL represent by-value copy-in semantics, and are mutable in the body the function. Slang was updated to give a static error on the original code to catch this kind of mistake (so that `this` parameters are unlike ordinary function parameters, and no longer mutable). Of course, sometimes users *want* a mutable `this` parameter. Rather than make a mutable `this` the default (there are arguments both for and against this), this change adds a new attribute `[mutating]` that can be put on a method (member function) to indicate that its `this` parameter should be an `in out` parameter: ```hlsl [mutating] void setState(int s) { state = s; } ``` The above will translate to, more or less: ```hlsl void Foo_setState(inout Foo this, int s) { this.state = s; } ``` One added detail is that `[mutating]` can also be used on interface requirements, with the same semantics. A `[mutating]` requirement can be satisfied with a `[mutating]` or non-`[mutating]` method, while a non-`[mutating]` requirement can't be satisfied with a `[mutating]` method (the call sites would not expect mutation to happen). The design of `[mutating]` here is heavily influenced by the equivalent `mutating` keyword in Swift. Notes on the implementation: * Adding the new attribute was straightforward using the existing support, but I had to change around where attributes get checked in the overall sequencing of static checks, because attributes were being checked *after* function bodies, but with this change I need to look at semantically-checked attributes to determine the mutability of `this` * The check to restrict it so that `[mutating]` methods cannot satisfy non-`[mutating]` requirements was easy to add, but it points out the fact that there is a huge TODO comment where the actual checking of method *signatures* is supposed to happen. That is a bug waiting to bite users and needs to be fixed! * While we had special-case logic to detect attempts to modify state accessed through an immutable `this` (e.g., `this.state = s`), that logic didn't trigger when the mutation happened through a function/operator call (e.g., `this.state += s`), so this change factors out the validation logic for that case and calls through to it from both the assignment and `out` argument cases. * The error message for the special-case check was updated to note that the user could apply `[mutating]` to their function declaration to get rid of the error. * The semantic checking logic for an explicit `this` expression was already walking up through the scopes (created during parsing) and looking for a scope that represents an outer type declaration that `this` might be referring to. We simply extend it to note when it passes through the scope for a function or similar declaration (`FunctionDeclBase`) and check for the `[mutating]` attribute. If the attribute is seen, it returns a mutable `this` expression, and otherwise leaves it immutable. * The IR lowering logic then needed to be updated so that when adding an IR-level parameter to represent `this`, it gives it the appropriate "direction" based on the attributes of the function declaration being lowered. The rest of the IR logic works as-is, because it will treat `this` just like an other parameter (whether it is `in` or `inout`). * This biggest chunk of work was the "implicit `this`" case, because ordinary name lookup may resolve an expression like `state` into `this.state`, so that the `this` expression comes out of "thin air." To handle this case, I extended the structure of the "breadcrumbs" that come along with a lookup result (the breadcrumbs are used for any case where a single identifier like `state` needs to be embellished to a more complex expression as a result of lookup), so that it can identify whether a `Breadcrumb::Kind::This` node comes from a `[mutating]` context or not. Similar to the logic for an explicit `this`, we handle this by noting when we pass through a `FunctionDeclBase` when moving up through scopes, and look for the `[mutating]` attribute on it. The rest of the work was just plumbing the additional state through.
1 parent 879ec1b commit dcd9f78

11 files changed

+202
-66
lines changed

source/slang/check.cpp

+79-48
Original file line numberDiff line numberDiff line change
@@ -475,11 +475,13 @@ namespace Slang
475475
}
476476

477477
RefPtr<Expr> createImplicitThisMemberExpr(
478-
Type* type,
479-
SourceLoc loc)
478+
Type* type,
479+
SourceLoc loc,
480+
LookupResultItem::Breadcrumb::ThisParameterMode thisParameterMode)
480481
{
481482
RefPtr<ThisExpr> expr = new ThisExpr();
482483
expr->type = type;
484+
expr->type.IsLeftValue = thisParameterMode == LookupResultItem::Breadcrumb::ThisParameterMode::Mutating;
483485
expr->loc = loc;
484486
return expr;
485487
}
@@ -528,14 +530,16 @@ namespace Slang
528530
{
529531
bb = createImplicitThisMemberExpr(
530532
GetTargetType(extensionDeclRef),
531-
loc);
533+
loc,
534+
breadcrumb->thisParameterMode);
532535
}
533536
else
534537
{
535538
auto type = DeclRefType::Create(getSession(), breadcrumb->declRef);
536539
bb = createImplicitThisMemberExpr(
537540
type,
538-
loc);
541+
loc,
542+
breadcrumb->thisParameterMode);
539543
}
540544
}
541545
break;
@@ -779,6 +783,10 @@ namespace Slang
779783
decl->SetCheckState(DeclCheckState::CheckingHeader);
780784
}
781785

786+
// Check the modifiers on the declaration first, in case
787+
// semantics of the body itself will depend on them.
788+
checkModifiers(decl);
789+
782790
// Use visitor pattern to dispatch to correct case
783791
dispatchDecl(decl);
784792

@@ -2197,12 +2205,6 @@ namespace Slang
21972205
EnusreAllDeclsRec(d);
21982206
}
21992207

2200-
// Do any semantic checking required on modifiers?
2201-
for (auto d : programNode->Members)
2202-
{
2203-
checkModifiers(d.Ptr());
2204-
}
2205-
22062208
if (pass == 0)
22072209
{
22082210
// now we can check all interface conformances
@@ -2236,6 +2238,14 @@ namespace Slang
22362238
DeclRef<CallableDecl> requiredMemberDeclRef,
22372239
RefPtr<WitnessTable> witnessTable)
22382240
{
2241+
if(satisfyingMemberDeclRef.getDecl()->HasModifier<MutatingAttribute>()
2242+
&& !requiredMemberDeclRef.getDecl()->HasModifier<MutatingAttribute>())
2243+
{
2244+
// A `[mutating]` method can't satisfy a non-`[mutating]` requirement,
2245+
// but vice-versa is okay.
2246+
return false;
2247+
}
2248+
22392249
// TODO: actually implement matching here. For now we'll
22402250
// just pretend that things are satisfied in order to make progress..
22412251
witnessTable->requirementDictionary.Add(
@@ -4599,6 +4609,44 @@ namespace Slang
45994609

46004610
//
46014611

4612+
/// Given an immutable `expr` used as an l-value emit a special diagnostic if it was derived from `this`.
4613+
void maybeDiagnoseThisNotLValue(Expr* expr)
4614+
{
4615+
// We will try to handle expressions of the form:
4616+
//
4617+
// e ::= "this"
4618+
// | e . name
4619+
// | e [ expr ]
4620+
//
4621+
// We will unwrap the `e.name` and `e[expr]` cases in a loop.
4622+
RefPtr<Expr> e = expr;
4623+
for(;;)
4624+
{
4625+
if(auto memberExpr = e.As<MemberExpr>())
4626+
{
4627+
e = memberExpr->BaseExpression;
4628+
}
4629+
else if(auto subscriptExpr = e.As<IndexExpr>())
4630+
{
4631+
e = subscriptExpr->BaseExpression;
4632+
}
4633+
else
4634+
{
4635+
break;
4636+
}
4637+
}
4638+
//
4639+
// Now we check to see if we have a `this` expression,
4640+
// and if it is immutable.
4641+
if(auto thisExpr = e.As<ThisExpr>())
4642+
{
4643+
if(!thisExpr->type.IsLeftValue)
4644+
{
4645+
getSink()->diagnose(thisExpr, Diagnostics::thisIsImmutableByDefault);
4646+
}
4647+
}
4648+
}
4649+
46024650
RefPtr<Expr> visitAssignExpr(AssignExpr* expr)
46034651
{
46044652
expr->left = CheckExpr(expr->left);
@@ -4622,40 +4670,7 @@ namespace Slang
46224670
// is immutable. We can give the user a bit more context into
46234671
// what is going on.
46244672
//
4625-
// We will try to handle expressions of the form:
4626-
//
4627-
// e ::= "this"
4628-
// | e . name
4629-
// | e [ expr ]
4630-
//
4631-
// We will unwrap the `e.name` and `e[expr]` cases in a loop.
4632-
RefPtr<Expr> e = expr->left;
4633-
for(;;)
4634-
{
4635-
if(auto memberExpr = e.As<MemberExpr>())
4636-
{
4637-
e = memberExpr->BaseExpression;
4638-
}
4639-
else if(auto subscriptExpr = e.As<IndexExpr>())
4640-
{
4641-
e = subscriptExpr->BaseExpression;
4642-
}
4643-
else
4644-
{
4645-
break;
4646-
}
4647-
}
4648-
//
4649-
// Now we check to see if we have a `this` expression,
4650-
// and if it is immutable.
4651-
if(auto thisExpr = e.As<ThisExpr>())
4652-
{
4653-
if(!thisExpr->type.IsLeftValue)
4654-
{
4655-
getSink()->diagnose(thisExpr, Diagnostics::thisIsImmutableByDefault);
4656-
}
4657-
}
4658-
4673+
maybeDiagnoseThisNotLValue(expr->left);
46594674
}
46604675
}
46614676
expr->type = type;
@@ -6298,7 +6313,10 @@ namespace Slang
62986313

62996314
LookupResultItem ctorItem;
63006315
ctorItem.declRef = ctorDeclRef;
6301-
ctorItem.breadcrumbs = new LookupResultItem::Breadcrumb(LookupResultItem::Breadcrumb::Kind::Member, typeItem.declRef, typeItem.breadcrumbs);
6316+
ctorItem.breadcrumbs = new LookupResultItem::Breadcrumb(
6317+
LookupResultItem::Breadcrumb::Kind::Member,
6318+
typeItem.declRef,
6319+
typeItem.breadcrumbs);
63026320

63036321
OverloadCandidate candidate;
63046322
candidate.flavor = OverloadCandidate::Flavor::Func;
@@ -7585,6 +7603,8 @@ namespace Slang
75857603
implicitCastExpr->Arguments[0]->type,
75867604
implicitCastExpr->type);
75877605
}
7606+
7607+
maybeDiagnoseThisNotLValue(argExpr);
75887608
}
75897609
}
75907610
else
@@ -8132,21 +8152,32 @@ namespace Slang
81328152
// expression.
81338153
RefPtr<Expr> visitThisExpr(ThisExpr* expr)
81348154
{
8155+
// A `this` expression will default to immutable.
8156+
expr->type.IsLeftValue = false;
8157+
81358158
// We will do an upwards search starting in the current
81368159
// scope, looking for a surrounding type (or `extension`)
81378160
// declaration that could be the referrant of the expression.
81388161
auto scope = expr->scope;
81398162
while (scope)
81408163
{
81418164
auto containerDecl = scope->containerDecl;
8142-
if (auto aggTypeDecl = containerDecl->As<AggTypeDecl>())
8165+
8166+
if( auto funcDeclBase = containerDecl->As<FunctionDeclBase>() )
8167+
{
8168+
if( funcDeclBase->HasModifier<MutatingAttribute>() )
8169+
{
8170+
expr->type.IsLeftValue = true;
8171+
}
8172+
}
8173+
else if (auto aggTypeDecl = containerDecl->As<AggTypeDecl>())
81438174
{
81448175
checkDecl(aggTypeDecl);
81458176

81468177
// Okay, we are using `this` in the context of an
81478178
// aggregate type, so the expression should be
81488179
// of the corresponding type.
8149-
expr->type = DeclRefType::Create(
8180+
expr->type.type = DeclRefType::Create(
81508181
getSession(),
81518182
makeDeclRef(aggTypeDecl));
81528183
return expr;
@@ -8165,7 +8196,7 @@ namespace Slang
81658196
// if there are multiple extensions in scope that add
81668197
// members with the same name...
81678198
//
8168-
expr->type = QualType(extensionDecl->targetType.type);
8199+
expr->type.type = extensionDecl->targetType.type;
81698200
return expr;
81708201
}
81718202

source/slang/core.meta.slang

+3
Original file line numberDiff line numberDiff line change
@@ -1235,3 +1235,6 @@ attribute_syntax [__vulkanRayPayload] : VulkanRayPayloadAttribute;
12351235

12361236
__attributeTarget(VarDeclBase)
12371237
attribute_syntax [__vulkanHitAttributes] : VulkanHitAttributesAttribute;
1238+
1239+
__attributeTarget(FunctionDeclBase)
1240+
attribute_syntax [mutating] : MutatingAttribute;

source/slang/core.meta.slang.h

+3
Original file line numberDiff line numberDiff line change
@@ -1253,3 +1253,6 @@ SLANG_RAW("attribute_syntax [__vulkanRayPayload] : VulkanRayPayloadAttribute;\n"
12531253
SLANG_RAW("\n")
12541254
SLANG_RAW("__attributeTarget(VarDeclBase)\n")
12551255
SLANG_RAW("attribute_syntax [__vulkanHitAttributes] : VulkanHitAttributesAttribute;\n")
1256+
SLANG_RAW("\n")
1257+
SLANG_RAW("__attributeTarget(FunctionDeclBase)\n")
1258+
SLANG_RAW("attribute_syntax [mutating] : MutatingAttribute;\n")

source/slang/diagnostic-defs.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ DIAGNOSTIC(30035, Error, componentOverloadTypeMismatch, "'$0': type of overloade
204204
DIAGNOSTIC(30041, Error, bitOperationNonIntegral, "bit operation: operand must be integral type.")
205205
DIAGNOSTIC(30047, Error, argumentExpectedLValue, "argument passed to parameter '$0' must be l-value.")
206206
DIAGNOSTIC(30048, Note, implicitCastUsedAsLValue, "argument was implicitly cast from '$0' to '$1', and Slang does not support using an implicit cast as an l-value")
207-
DIAGNOSTIC(30049, Note, thisIsImmutableByDefault, "a 'this' parameter is currently immutable in Slang")
207+
DIAGNOSTIC(30049, Note, thisIsImmutableByDefault, "a 'this' parameter is an immutable parameter by default in Slang; apply the `[mutating]` attribute to the function declaration to opt in to a mutable `this`")
208208

209209
DIAGNOSTIC(30051, Error, invalidValueForArgument, "invalid value for argument '$0'")
210210
DIAGNOSTIC(30052, Error, invalidSwizzleExpr, "invalid swizzle pattern '$0' on type '$1'")

source/slang/lookup.cpp

+18-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ DeclRef<ExtensionDecl> ApplyExtensionToType(
2020
struct BreadcrumbInfo
2121
{
2222
LookupResultItem::Breadcrumb::Kind kind;
23+
LookupResultItem::Breadcrumb::ThisParameterMode thisParameterMode = LookupResultItem::Breadcrumb::ThisParameterMode::Default;
2324
DeclRef<Decl> declRef;
2425
BreadcrumbInfo* prev = nullptr;
2526
};
@@ -161,7 +162,8 @@ LookupResultItem CreateLookupResultItem(
161162
breadcrumbs = new LookupResultItem::Breadcrumb(
162163
bb->kind,
163164
bb->declRef,
164-
breadcrumbs);
165+
breadcrumbs,
166+
bb->thisParameterMode);
165167
}
166168
item.breadcrumbs = breadcrumbs;
167169
return item;
@@ -433,6 +435,8 @@ void DoLookupImpl(
433435
LookupRequest const& request,
434436
LookupResult& result)
435437
{
438+
auto thisParameterMode = LookupResultItem::Breadcrumb::ThisParameterMode::Default;
439+
436440
auto scope = request.scope;
437441
auto endScope = request.endScope;
438442
for (;scope != endScope; scope = scope->parent)
@@ -464,6 +468,7 @@ void DoLookupImpl(
464468
if (auto aggTypeDeclRef = containerDeclRef.As<AggTypeDeclBase>())
465469
{
466470
breadcrumb.kind = LookupResultItem::Breadcrumb::Kind::This;
471+
breadcrumb.thisParameterMode = thisParameterMode;
467472
breadcrumb.declRef = aggTypeDeclRef;
468473
breadcrumb.prev = nullptr;
469474

@@ -488,6 +493,18 @@ void DoLookupImpl(
488493
DoLocalLookupImpl(
489494
session,
490495
name, containerDeclRef, request, result, breadcrumbs);
496+
497+
if( auto funcDeclRef = containerDeclRef.As<FunctionDeclBase>() )
498+
{
499+
if( funcDeclRef.getDecl()->HasModifier<MutatingAttribute>() )
500+
{
501+
thisParameterMode = LookupResultItem::Breadcrumb::ThisParameterMode::Mutating;
502+
}
503+
else
504+
{
505+
thisParameterMode = LookupResultItem::Breadcrumb::ThisParameterMode::Default;
506+
}
507+
}
491508
}
492509

493510
if (result.isValid())

source/slang/lower-to-ir.cpp

+18-12
Original file line numberDiff line numberDiff line change
@@ -4494,19 +4494,10 @@ struct DeclLoweringVisitor : DeclVisitor<DeclLoweringVisitor, LoweredValInfo>
44944494
// parameter for the enclosing type:
44954495
//
44964496
void addThisParameter(
4497+
ParameterDirection direction,
44974498
Type* type,
44984499
ParameterLists* ioParameterLists)
44994500
{
4500-
// For now we make any `this` parameter default to `in`. Eventually
4501-
// we should add a way for the user to opt in to mutability of `this`
4502-
// in cases where they really want it.
4503-
//
4504-
// Note: an alternative here might be to have the built-in types like
4505-
// `Texture2D` actually use `class` declarations and say that the
4506-
// `this` parameter for a class type is always `in`, while `struct`
4507-
// types can default to `in out`.
4508-
ParameterDirection direction = kParameterDirection_In;
4509-
45104501
ParameterInfo info;
45114502
info.type = type;
45124503
info.decl = nullptr;
@@ -4516,6 +4507,7 @@ struct DeclLoweringVisitor : DeclVisitor<DeclLoweringVisitor, LoweredValInfo>
45164507
ioParameterLists->params.Add(info);
45174508
}
45184509
void addThisParameter(
4510+
ParameterDirection direction,
45194511
AggTypeDecl* typeDecl,
45204512
ParameterLists* ioParameterLists)
45214513
{
@@ -4526,6 +4518,7 @@ struct DeclLoweringVisitor : DeclVisitor<DeclLoweringVisitor, LoweredValInfo>
45264518
auto declRef = createDefaultSpecializedDeclRef(typeDecl);
45274519
RefPtr<Type> type = DeclRefType::Create(context->getSession(), declRef);
45284520
addThisParameter(
4521+
direction,
45294522
type,
45304523
ioParameterLists);
45314524
}
@@ -4558,13 +4551,26 @@ struct DeclLoweringVisitor : DeclVisitor<DeclLoweringVisitor, LoweredValInfo>
45584551
// parameter corresponding to the outer declaration.
45594552
if( innerMode != kParameterListCollectMode_Static )
45604553
{
4554+
// For now we make any `this` parameter default to `in`.
4555+
//
4556+
ParameterDirection direction = kParameterDirection_In;
4557+
//
4558+
// Applications can opt in to a mutable `this` parameter,
4559+
// by applying the `[mutating]` attribute to their
4560+
// declaration.
4561+
//
4562+
if( decl->HasModifier<MutatingAttribute>() )
4563+
{
4564+
direction = kParameterDirection_InOut;
4565+
}
4566+
45614567
if( auto aggTypeDecl = dynamic_cast<AggTypeDecl*>(parentDecl) )
45624568
{
4563-
addThisParameter(aggTypeDecl, ioParameterLists);
4569+
addThisParameter(direction, aggTypeDecl, ioParameterLists);
45644570
}
45654571
else if( auto extensionDecl = dynamic_cast<ExtensionDecl*>(parentDecl) )
45664572
{
4567-
addThisParameter(extensionDecl->targetType, ioParameterLists);
4573+
addThisParameter(direction, extensionDecl->targetType, ioParameterLists);
45684574
}
45694575
}
45704576
}

source/slang/modifier-defs.h

+6
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,12 @@ SIMPLE_SYNTAX_CLASS(VulkanRayPayloadAttribute, Attribute)
399399
// intersection shader to pass hit attribute information.
400400
SIMPLE_SYNTAX_CLASS(VulkanHitAttributesAttribute, Attribute)
401401

402+
// A `[mutating]` attribute, which indicates that a member
403+
// function is allowed to modify things through its `this`
404+
// argument.
405+
//
406+
SIMPLE_SYNTAX_CLASS(MutatingAttribute, Attribute)
407+
402408
// HLSL modifiers for geometry shader input topology
403409
SIMPLE_SYNTAX_CLASS(HLSLGeometryShaderInputPrimitiveTypeModifier, Modifier)
404410
SIMPLE_SYNTAX_CLASS(HLSLPointModifier , HLSLGeometryShaderInputPrimitiveTypeModifier)

0 commit comments

Comments
 (0)