Skip to content

Commit

Permalink
Fix yielding objects from an iterator record whose copy mutates them (#…
Browse files Browse the repository at this point in the history
…26230)

Closes #11299. Closes
#14246.

Thank you @vasslitvinov for the initial investigation and for suggesting
the direction. Specifically, this PR introduces loop pragmas and uses
them to make certain variables mutable, addressing the constness errors.

This PR fixes an issue in which yielding things-that-contain-`owned`
(but not `owned` by itself) caused constness errors in the compiler,
with the following rough shape.

E.g., in the following program:

```Chapel
iter asdf() do yield (new owned RootClass(),);
var fdsa = asdf();
compilerWarning(fdsa.type:string);
writeln(fdsa);
```

Produced the following error prior to this PR:

```
$CHPL_HOME/modules/internal/ChapelArray.chpl:3573: In function 'chpl__initCopy':
$CHPL_HOME/modules/internal/ChapelArray.chpl:3611: error: const actual is passed to 'ref' formal 'x' of chpl__initCopy()
  vasstmp.chpl:2: called as chpl__initCopy(ir: _ir_asdf, definedConst: bool)
note: generic instantiations are underlined in the above callstack
vasstmp.chpl:1: In module 'vasstmp':
vasstmp.chpl:3: note: [domain(1,int(64),one)] 1*owned RootClass
```

Reviewed by @jabraham17 (parser) and @vasslitvinov (the rest) -- thanks!

## Background

The problem is caused by the fact that copying of elements of types like
`owned C?` mutates the element. This happens because the value (pointer)
gets _moved_ into the new copy of the smart pointer, resetting the old
copy to `nil`.

```Chapel
var x = new C?();
var y = x; // fine, but x is now nil
```

This, in turn, means that copying `x` requires `x` to be mutable.

```Chapel
const x = new C?();
var y = x; // not fine, constness error: const x is passed to ref formal of init=
```

This is all fine and good. However, another component of the problem is
that Chapel by default makes loop indices constant.

```Chapel
for x in someIterator() { // x is 'const' 
  var y = x; // would not be fine but there's a compiler hack; see the following paragraph
}
```

As you may read in the above comment, the code above ought not to work
for the same reason as the `const x` snippet: copying `x` would mutate
it, but `x` is constant. However, the Chapel compiler actually
explicitly overrides the constness rule for a restricted set of types
(those with the `FLAG_COPY_MUTATES` flag set):


https://github.com/chapel-lang/chapel/blob/ed9522e3cdbf9aab881b0602dcb2d070ff650079/compiler/resolution/functionResolution.cpp#L9474-L9476

`FLAG_COPY_MUTATES` is supposed to apply to all types which get mutated
by `init=` (which includes `owned`, but also any record that has a
default `init=` and an `owned` __field__, since copying such a record
would also copy, and mutate, its fields). However, at this point,
`FLAG_COPY_MUTATES` is not propagated, so _only `owned`_ is affected by
this hack. Other types that are mutated by copying issue constness
errors here.

It's possible to change this logic to ensure that `FLAG_COPY_MUTATES` is
propagated to other types. However, this would constitute a significant
semantic change, and make loop indices of a large variety of types
(anything with an `owned` field) mutable when iterated over. This is
undesirable; moreover, the current computation that propagates
`FLAG_COPY_MUTATES` is incorrect / overly aggressive. For instance,
`set` is marked `FLAG_COPY_MUTATES`, even though it supports a
non-mutating copy constructor. This is another reason to avoid leaning
on the flag.

Instead, we observe that it's sound to leave variables yielded by value
mutable. Though we don't want to change user-facing behavior (which
until now has kept loop indices constant), we would like to be able to
opt into it for copying functions in `ChapelArray`, which which emit the
constness errors this PR aims to resolve. if the index variables of the
loops that back the `ChapelArray` functions are mutable, they can be
safely copied into the array-being-initialized, mutating the (temporary
and subsequently unused) index variable.

## Implementation
This PR achieves mutable loop index variables by adding support for loop
flags (`#pragma "bla" for ...`). To this, the PR:

* Introduces two new flags, `loop indices mutable` and `loop index
mutable`. The former is applied to loops; however, since in the
production compiler pragmas can only be applied to symbols, the latter
is applied to each index variable. Index variables marked 'loop index
mutable' do not get set to `const`.
* Applies the `loop indices mutable` pragma to affected functions in
`ChapelArray`. This fixes the constness errors when copying out of these
indices into a new array.
* Changes loop attribute group parsing to consume pragmas even if
attributes were not encountered. This enables full-fledged loop pragmas
and resolve an inconsistency (pragmas would be consumed if attributes
were present, but left alone otherwise).
* Resolves a parser state bug present on `main`, but triggered more
often under the changes from the previous bullet. Prior to this PR,
pragmas for variable declarations were collected __after__ parsing the
entire variable declaration. This meant they remained in parser state
throughout parsing the initialization expression. As a result, any
statement contained therein (e.g. via FCF, lambda, etc.) would have
these pragmas applied to it instead. Since loop expressions now also
trigger this behavior, the following code was incorrectly parsed:

    ```Chapel
    pragma "bla" var x = for i in 1..10 do i;
   ``` 

* To fix this, this PR changes several productions in the parser to
eagerly consume attribute groups (before moving on to the type
expression). This matches several other productions already in place
(forwarding declarations, procedures, etc.).

## Testing
- [x] paratest
  • Loading branch information
DanilaFe authored Nov 13, 2024
2 parents ed9522e + 173e6be commit e1b1d03
Show file tree
Hide file tree
Showing 24 changed files with 5,113 additions and 5,080 deletions.
13 changes: 13 additions & 0 deletions compiler/passes/convert-help.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@

#include "convert-help.h"

#include "astutil.h"
#include "chpl/parsing/parsing-queries.h"
#include "chpl/uast/chpl-syntax-printer.h"
#include "LoopStmt.h"

#include "stmt.h"

Expand Down Expand Up @@ -183,6 +185,17 @@ void LoopAttributeInfo::insertPrimitivesBlockAtHead(UastConverter& converter,
}
}

void LoopAttributeInfo::applyToLoop(UastConverter& converter, Expr* indices, BlockStmt* body) {
insertPrimitivesBlockAtHead(converter, body);

if (loopIndicesMutable) {
std::vector<DefExpr*> defExprs;
collectDefExprs(indices, defExprs);
for (auto defExpr : defExprs) {
defExpr->sym->addFlag(FLAG_LOOP_INDEX_MUTABLE);
}
}
}

const char* convertLinkageNameAstr(const uast::Decl* node) {
if (auto linkageName = node->linkageName()) {
Expand Down
5 changes: 5 additions & 0 deletions compiler/passes/convert-help.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ struct LoopAttributeInfo {
const uast::Attribute* blockSizeAttr = nullptr;
// The @gpu.itersPerThread attribute, if one is provided by the user.
const uast::Attribute* itersPerThreadAttr = nullptr;
// If true, skip marking loop indices 'const'.
bool loopIndicesMutable = false;

private:
LLVMMetadataPtr tupleToLLVMMetadata(Context* context,
Expand All @@ -76,6 +78,8 @@ struct LoopAttributeInfo {
LoopAttributeInfo into;
into.readLlvmAttributes(context, attrs);
into.readNativeGpuAttributes(attrs);
into.loopIndicesMutable =
attrs->hasPragma(uast::PragmaTag::PRAGMA_LOOP_INDICES_MUTABLE);

return into;
}
Expand Down Expand Up @@ -109,6 +113,7 @@ struct LoopAttributeInfo {
bool insertItersPerThreadCall(UastConverter& converter, BlockStmt* body);
BlockStmt* createPrimitivesBlock(UastConverter& converter);
void insertPrimitivesBlockAtHead(UastConverter& converter, BlockStmt* body);
void applyToLoop(UastConverter& converter, Expr* indices, BlockStmt* body);
};

const char* convertLinkageNameAstr(const uast::Decl* node);
Expand Down
8 changes: 5 additions & 3 deletions compiler/passes/convert-uast.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1536,7 +1536,7 @@ struct Converter final : UastConverter {
}

auto loopAttributes = LoopAttributeInfo::fromExplicitLoop(context, node);
loopAttributes.insertPrimitivesBlockAtHead(*this, body);
loopAttributes.applyToLoop(*this, indices, body);
return ForallStmt::build(indices, iterator, intents, body, zippered,
serialOK);
}
Expand Down Expand Up @@ -1617,6 +1617,8 @@ struct Converter final : UastConverter {
BlockStmt* block = toBlockStmt(body);
INT_ASSERT(block);

auto loopAttributes = LoopAttributeInfo::fromExplicitLoop(context, node);
loopAttributes.applyToLoop(*this, index, block);
ret = ForLoop::buildForLoop(index, iteratorExpr, block, zippered,
isForExpr, extractLlvmAttributesAndRejectOthers(context, node));
}
Expand Down Expand Up @@ -1664,7 +1666,7 @@ struct Converter final : UastConverter {
bool serialOK = false;

auto loopAttributes = LoopAttributeInfo::fromExplicitLoop(context, node);
loopAttributes.insertPrimitivesBlockAtHead(*this, body);
loopAttributes.applyToLoop(*this, indices, body);
return ForallStmt::build(indices, iterator, intents, body, zippered,
serialOK);
}
Expand All @@ -1690,7 +1692,7 @@ struct Converter final : UastConverter {
} else {
auto body = createBlockWithStmts(node->stmts(), node->blockStyle());
auto loopAttributes = LoopAttributeInfo::fromExplicitLoop(context, node);
loopAttributes.insertPrimitivesBlockAtHead(*this, body);
loopAttributes.applyToLoop(*this, indices, body);
ret = ForLoop::buildForeachLoop(indices, iteratorExpr, intents, body,
zippered,
isForExpr, std::move(loopAttributes.llvmMetadata));
Expand Down
7 changes: 5 additions & 2 deletions compiler/resolution/functionResolution.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9458,11 +9458,14 @@ static void resolveMoveForRhsSymExpr(CallExpr* call, SymExpr* rhs) {
if (rhsSym->hasFlag(FLAG_INDEX_OF_INTEREST) == true) {
Type* rhsType = rhs->typeInfo();

if (lhsSym->hasFlag(FLAG_INDEX_VAR) ||
if ((lhsSym->hasFlag(FLAG_INDEX_VAR) &&
lhsSym->hasFlag(FLAG_LOOP_INDEX_MUTABLE) == false) ||
// non-zip forall over a standalone iterator
(lhsSym->hasFlag(FLAG_TEMP) &&
rhsSym->hasFlag(FLAG_INDEX_VAR))) {
rhsSym->hasFlag(FLAG_INDEX_VAR) &&
rhsSym->hasFlag(FLAG_LOOP_INDEX_MUTABLE) == false)) {

// ... and the loop didn't ask to be kept mutable via pragma
// ... and not of a reference type
// ... and not an array (arrays are always yielded by reference)
// see also resolveIdxVar() / defaultIntentYieldsConst()
Expand Down
3 changes: 2 additions & 1 deletion compiler/resolution/preFold.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3124,7 +3124,8 @@ static Expr* resolveTupleIndexing(CallExpr* call, Symbol* baseVar) {
// And it's not an array (arrays are always yielded by reference)
// - see boundaries() in release/examples/benchmarks/miniMD/miniMD.
if (!fieldType->symbol->hasFlag(FLAG_ARRAY) &&
!fieldType->symbol->hasFlag(FLAG_COPY_MUTATES)) {
!fieldType->symbol->hasFlag(FLAG_COPY_MUTATES) &&
!destSE->symbol()->hasFlag(FLAG_LOOP_INDEX_MUTABLE)) {
destSE->symbol()->addFlag(FLAG_CONST);
}
} else {
Expand Down
3 changes: 3 additions & 0 deletions frontend/include/chpl/uast/PragmaList.h
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,9 @@ PRAGMA(NO_GPU_CODEGEN, ypr, "no gpu codegen", ncm)
// See ORDER_INDEPENDENT_YIELDING_LOOPS below
PRAGMA(NOT_ORDER_INDEPENDENT_YIELDING_LOOPS, ypr, "not order independent yielding loops", "yielding loops in iterator itself are not order independent")

PRAGMA(LOOP_INDICES_MUTABLE, ypr, "loop indices mutable", "do not opportunistically mark loop indices as 'const'")
PRAGMA(LOOP_INDEX_MUTABLE, ypr, "loop index mutable", "do not mark this loop index as 'const'")

// See POD below
PRAGMA(NOT_POD, ypr, "not plain old data", "bit copy overridden")

Expand Down
5 changes: 4 additions & 1 deletion frontend/lib/parsing/ParserContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ struct ParserContext {

// If attributes do not exist yet, returns nullptr.
owned<AttributeGroup> buildAttributeGroup(YYLTYPE locationOfDecl);
// Same as buildAttributeGroup, but resets the state and pushes onto group stack
void buildAndPushAttributeGroup(YYLTYPE locationOfDecl);

PODUniqueString notePragma(YYLTYPE loc, AstNode* pragmaStr);
void noteDeprecation(YYLTYPE loc, MaybeNamedActualList* actuals);
void noteUnstable(YYLTYPE loc, MaybeNamedActualList* actuals);
Expand Down Expand Up @@ -641,7 +644,7 @@ struct ParserContext {

// Given a list of vars, build either a single var or a multi-decl.
CommentsAndStmt
buildVarOrMultiDeclStmt(YYLTYPE locEverything, ParserExprList* vars);
buildVarOrMultiDeclStmt(YYLTYPE locEverything, AttributeGroup* attributeGroup, ParserExprList* vars);

TypeDeclParts enterScopeAndBuildTypeDeclParts(YYLTYPE locStart,
YYLTYPE locName,
Expand Down
17 changes: 15 additions & 2 deletions frontend/lib/parsing/ParserContextImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,15 @@ owned<AttributeGroup> ParserContext::buildAttributeGroup(YYLTYPE locationOfDecl)
return node;
}


void ParserContext::buildAndPushAttributeGroup(YYLTYPE locationOfDecl) {
auto attributeGroup = this->buildAttributeGroup(std::move(locationOfDecl));
if (attributeGroup != nullptr) {
this->resetAttributeGroupPartsState();
}
this->loopAttributes.push_back(std::move(attributeGroup));
}

PODUniqueString ParserContext::notePragma(YYLTYPE loc,
AstNode* pragmaStr) {
auto ret = PODUniqueString::get();
Expand Down Expand Up @@ -477,6 +486,9 @@ ParserContext::buildPragmaStmt(YYLTYPE loc, CommentsAndStmt cs) {
// do nothing on an erroneous statement
// Clean up the attribute parts.
resetAttributeGroupPartsState();
} else if (cs.stmt && cs.stmt->isLoop()) {
// Loop pragmas are captured by the loop's own attribute handling logic.
// Not an error, and resetAttributeGroupPartsState() has been invoked.
} else {
// TODO: The original builder also states the first pragma.
CHPL_PARSER_REPORT(this, CannotAttachPragmas, loc, cs.stmt);
Expand Down Expand Up @@ -2746,7 +2758,10 @@ buildSingleUseStmt(YYLTYPE locEverything, YYLTYPE locVisibilityClause,
// Given a list of vars, build either a single var or a multi-decl.
CommentsAndStmt
ParserContext::buildVarOrMultiDeclStmt(YYLTYPE locEverything,
AttributeGroup* attributeGroupPtr,
ParserExprList* vars) {
auto attributeGroup = toOwned(attributeGroupPtr);

int numDecls = 0;
Decl* firstDecl = nullptr;
Decl* lastDecl = nullptr;
Expand Down Expand Up @@ -2775,7 +2790,6 @@ ParserContext::buildVarOrMultiDeclStmt(YYLTYPE locEverything,
delete vars;
// for single element decls, we attach the attribute group to the decl
// *note that this places the AttributeGroup as the LAST child of the decl
auto attributeGroup = buildAttributeGroup(locEverything);
if (attributeGroup) {
lastDecl->attachAttributeGroup(std::move(attributeGroup));
}
Expand All @@ -2801,7 +2815,6 @@ ParserContext::buildVarOrMultiDeclStmt(YYLTYPE locEverything,
CHPL_PARSER_REPORT(this, MultipleExternalRenaming, locEverything);
}
}
auto attributeGroup = buildAttributeGroup(locEverything);
auto multi = MultiDecl::build(builder, convertLocation(locEverything),
std::move(attributeGroup),
visibility,
Expand Down
Loading

0 comments on commit e1b1d03

Please sign in to comment.