-
Notifications
You must be signed in to change notification settings - Fork 15.2k
[OpenACC] Implement 'tile' loop count/tightly nested loop requirement #111038
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
the 'tile' clause requires that it be followed by N (where N is the
number of size expressions) 'tightly nested loops'. This means the
same as it does in 'collapse', so much of the implementation is
simliar/shared with that.
|
@llvm/pr-subscribers-clang Author: Erich Keane (erichkeane) Changesthe 'tile' clause requires that it be followed by N (where N is the Patch is 32.39 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/111038.diff 7 Files Affected:
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index 59dc81cfeb7111..583475327c5227 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -12670,19 +12670,19 @@ def err_acc_size_expr_value
: Error<
"OpenACC 'tile' clause size expression must be %select{an asterisk "
"or a constant expression|positive integer value, evaluated to %1}0">;
-def err_acc_invalid_in_collapse_loop
- : Error<"%select{OpenACC '%1' construct|while loop|do loop}0 cannot appear "
- "in intervening code of a 'loop' with a 'collapse' clause">;
-def note_acc_collapse_clause_here
- : Note<"active 'collapse' clause defined here">;
-def err_acc_collapse_multiple_loops
+def err_acc_invalid_in_loop
+ : Error<"%select{OpenACC '%2' construct|while loop|do loop}0 cannot appear "
+ "in intervening code of a 'loop' with a '%1' clause">;
+def note_acc_active_clause_here
+ : Note<"active '%0' clause defined here">;
+def err_acc_clause_multiple_loops
: Error<"more than one for-loop in a loop associated with OpenACC 'loop' "
- "construct with a 'collapse' clause">;
-def err_acc_collapse_insufficient_loops
- : Error<"'collapse' clause specifies a loop count greater than the number "
+ "construct with a '%select{collapse|tile}0' clause">;
+def err_acc_insufficient_loops
+ : Error<"'%0' clause specifies a loop count greater than the number "
"of available loops">;
-def err_acc_collapse_intervening_code
- : Error<"inner loops must be tightly nested inside a 'collapse' clause on "
+def err_acc_intervening_code
+ : Error<"inner loops must be tightly nested inside a '%0' clause on "
"a 'loop' construct">;
// AMDGCN builtins diagnostics
diff --git a/clang/include/clang/Sema/SemaOpenACC.h b/clang/include/clang/Sema/SemaOpenACC.h
index d25c52ec3be43a..97386d2378b758 100644
--- a/clang/include/clang/Sema/SemaOpenACC.h
+++ b/clang/include/clang/Sema/SemaOpenACC.h
@@ -42,6 +42,23 @@ class SemaOpenACC : public SemaBase {
/// above collection.
bool InsideComputeConstruct = false;
+ /// Certain clauses care about the same things that aren't specific to the
+ /// individual clause, but can be shared by a few, so store them here. All
+ /// require a 'no intervening constructs' rule, so we know they are all from
+ /// the same 'place'.
+ struct LoopCheckingInfo {
+ /// Records whether we've seen the top level 'for'. We already diagnose
+ /// later that the 'top level' is a for loop, so we use this to suppress the
+ /// 'collapse inner loop not a 'for' loop' diagnostic.
+ LLVM_PREFERRED_TYPE(bool)
+ unsigned TopLevelLoopSeen : 1;
+
+ /// Records whether this 'tier' of the loop has already seen a 'for' loop,
+ /// used to diagnose if there are multiple 'for' loops at any one level.
+ LLVM_PREFERRED_TYPE(bool)
+ unsigned CurLevelHasLoopAlready : 1;
+ } LoopInfo{/*TopLevelLoopSeen=*/false, /*CurLevelHasLoopAlready=*/false};
+
/// The 'collapse' clause requires quite a bit of checking while
/// parsing/instantiating its body, so this structure/object keeps all of the
/// necessary information as we do checking. This should rarely be directly
@@ -59,25 +76,27 @@ class SemaOpenACC : public SemaBase {
/// else it should be 'N' minus the current depth traversed.
std::optional<llvm::APSInt> CurCollapseCount;
- /// Records whether we've seen the top level 'for'. We already diagnose
- /// later that the 'top level' is a for loop, so we use this to suppress the
- /// 'collapse inner loop not a 'for' loop' diagnostic.
- LLVM_PREFERRED_TYPE(bool)
- unsigned TopLevelLoopSeen : 1;
-
- /// Records whether this 'tier' of the loop has already seen a 'for' loop,
- /// used to diagnose if there are multiple 'for' loops at any one level.
- LLVM_PREFERRED_TYPE(bool)
- unsigned CurLevelHasLoopAlready : 1;
-
/// Records whether we've hit a CurCollapseCount of '0' on the way down,
/// which allows us to diagnose if the value of 'N' is too large for the
/// current number of 'for' loops.
- LLVM_PREFERRED_TYPE(bool)
- unsigned CollapseDepthSatisfied : 1;
- } CollapseInfo{nullptr, std::nullopt, /*TopLevelLoopSeen=*/false,
- /*CurLevelHasLoopAlready=*/false,
- /*CollapseDepthSatisfied=*/true};
+ bool CollapseDepthSatisfied = true;
+ } CollapseInfo;
+
+ /// The 'tile' clause requires a bit of additional checking as well, so like
+ /// the `CollapseCheckingInfo`, ensure we maintain information here too.
+ struct TileCheckingInfo {
+ OpenACCTileClause *ActiveTile = nullptr;
+
+ /// This is the number of expressions on a 'tile' clause. This doesn't have
+ /// to be an APSInt because it isn't the result of a constexpr, just by our
+ /// own counting of elements.
+ std::optional<unsigned> CurTileCount;
+
+ /// Records whether we've hit a 'CurTileCount' of '0' on the wya down,
+ /// which allows us to diagnose if the number of arguments is too large for
+ /// the current number of 'for' loops.
+ bool TileDepthSatisfied = true;
+ } TileInfo;
public:
// Redeclaration of the version in OpenACCClause.h.
@@ -537,12 +556,15 @@ class SemaOpenACC : public SemaBase {
/// into a loop (for, etc) inside the construct.
class LoopInConstructRAII {
SemaOpenACC &SemaRef;
+ LoopCheckingInfo OldLoopInfo;
CollapseCheckingInfo OldCollapseInfo;
+ TileCheckingInfo OldTileInfo;
bool PreserveDepth;
public:
LoopInConstructRAII(SemaOpenACC &SemaRef, bool PreserveDepth = true)
- : SemaRef(SemaRef), OldCollapseInfo(SemaRef.CollapseInfo),
+ : SemaRef(SemaRef), OldLoopInfo(SemaRef.LoopInfo),
+ OldCollapseInfo(SemaRef.CollapseInfo), OldTileInfo(SemaRef.TileInfo),
PreserveDepth(PreserveDepth) {}
~LoopInConstructRAII() {
// The associated-statement level of this should NOT preserve this, as it
@@ -551,12 +573,20 @@ class SemaOpenACC : public SemaBase {
bool CollapseDepthSatisified =
PreserveDepth ? SemaRef.CollapseInfo.CollapseDepthSatisfied
: OldCollapseInfo.CollapseDepthSatisfied;
+ bool TileDepthSatisfied = PreserveDepth
+ ? SemaRef.TileInfo.TileDepthSatisfied
+ : OldTileInfo.TileDepthSatisfied;
bool CurLevelHasLoopAlready =
- PreserveDepth ? SemaRef.CollapseInfo.CurLevelHasLoopAlready
- : OldCollapseInfo.CurLevelHasLoopAlready;
+ PreserveDepth ? SemaRef.LoopInfo.CurLevelHasLoopAlready
+ : OldLoopInfo.CurLevelHasLoopAlready;
+
+ SemaRef.LoopInfo = OldLoopInfo;
SemaRef.CollapseInfo = OldCollapseInfo;
+ SemaRef.TileInfo = OldTileInfo;
+
SemaRef.CollapseInfo.CollapseDepthSatisfied = CollapseDepthSatisified;
- SemaRef.CollapseInfo.CurLevelHasLoopAlready = CurLevelHasLoopAlready;
+ SemaRef.TileInfo.TileDepthSatisfied = TileDepthSatisfied;
+ SemaRef.LoopInfo.CurLevelHasLoopAlready = CurLevelHasLoopAlready;
}
};
@@ -577,6 +607,9 @@ class SemaOpenACC : public SemaBase {
void SetCollapseInfoBeforeAssociatedStmt(
ArrayRef<const OpenACCClause *> UnInstClauses,
ArrayRef<OpenACCClause *> Clauses);
+ void SetTileInfoBeforeAssociatedStmt(
+ ArrayRef<const OpenACCClause *> UnInstClauses,
+ ArrayRef<OpenACCClause *> Clauses);
~AssociatedStmtRAII();
};
};
diff --git a/clang/lib/Sema/SemaOpenACC.cpp b/clang/lib/Sema/SemaOpenACC.cpp
index f3840c6ffba82b..66f8029a2754b9 100644
--- a/clang/lib/Sema/SemaOpenACC.cpp
+++ b/clang/lib/Sema/SemaOpenACC.cpp
@@ -1128,6 +1128,7 @@ SemaOpenACC::AssociatedStmtRAII::AssociatedStmtRAII(
SemaRef.ParentlessLoopConstructs.swap(ParentlessLoopConstructs);
} else if (DirKind == OpenACCDirectiveKind::Loop) {
SetCollapseInfoBeforeAssociatedStmt(UnInstClauses, Clauses);
+ SetTileInfoBeforeAssociatedStmt(UnInstClauses, Clauses);
}
}
@@ -1136,8 +1137,9 @@ void SemaOpenACC::AssociatedStmtRAII::SetCollapseInfoBeforeAssociatedStmt(
ArrayRef<OpenACCClause *> Clauses) {
// Reset this checking for loops that aren't covered in a RAII object.
- SemaRef.CollapseInfo.CurLevelHasLoopAlready = false;
+ SemaRef.LoopInfo.CurLevelHasLoopAlready = false;
SemaRef.CollapseInfo.CollapseDepthSatisfied = true;
+ SemaRef.TileInfo.TileDepthSatisfied = true;
// We make sure to take an optional list of uninstantiated clauses, so that
// we can check to make sure we don't 'double diagnose' in the event that
@@ -1176,6 +1178,26 @@ void SemaOpenACC::AssociatedStmtRAII::SetCollapseInfoBeforeAssociatedStmt(
cast<ConstantExpr>(LoopCount)->getResultAsAPSInt();
}
+void SemaOpenACC::AssociatedStmtRAII::SetTileInfoBeforeAssociatedStmt(
+ ArrayRef<const OpenACCClause *> UnInstClauses,
+ ArrayRef<OpenACCClause *> Clauses) {
+ // We don't diagnose if this is during instantiation, since the only thing we
+ // care about is the number of arguments, which we can figure out without
+ // instantiation, so we don't want to double-diagnose.
+ if (UnInstClauses.size() > 0)
+ return;
+ auto *TileClauseItr =
+ llvm::find_if(Clauses, llvm::IsaPred<OpenACCTileClause>);
+
+ if (Clauses.end() == TileClauseItr)
+ return;
+
+ OpenACCTileClause *TileClause = cast<OpenACCTileClause>(*TileClauseItr);
+ SemaRef.TileInfo.ActiveTile = TileClause;
+ SemaRef.TileInfo.TileDepthSatisfied = false;
+ SemaRef.TileInfo.CurTileCount = TileClause->getSizeExprs().size();
+}
+
SemaOpenACC::AssociatedStmtRAII::~AssociatedStmtRAII() {
SemaRef.InsideComputeConstruct = WasInsideComputeConstruct;
if (DirKind == OpenACCDirectiveKind::Parallel ||
@@ -1771,38 +1793,66 @@ void SemaOpenACC::ActOnWhileStmt(SourceLocation WhileLoc) {
if (!getLangOpts().OpenACC)
return;
- if (!CollapseInfo.TopLevelLoopSeen)
+ if (!LoopInfo.TopLevelLoopSeen)
return;
if (CollapseInfo.CurCollapseCount && *CollapseInfo.CurCollapseCount > 0) {
- Diag(WhileLoc, diag::err_acc_invalid_in_collapse_loop) << /*while loop*/ 1;
+ Diag(WhileLoc, diag::err_acc_invalid_in_loop)
+ << /*while loop*/ 1 << OpenACCClauseKind::Collapse;
assert(CollapseInfo.ActiveCollapse && "Collapse count without object?");
Diag(CollapseInfo.ActiveCollapse->getBeginLoc(),
- diag::note_acc_collapse_clause_here);
+ diag::note_acc_active_clause_here)
+ << OpenACCClauseKind::Collapse;
// Remove the value so that we don't get cascading errors in the body. The
// caller RAII object will restore this.
CollapseInfo.CurCollapseCount = std::nullopt;
}
+
+ if (TileInfo.CurTileCount && *TileInfo.CurTileCount > 0) {
+ Diag(WhileLoc, diag::err_acc_invalid_in_loop)
+ << /*while loop*/ 1 << OpenACCClauseKind::Tile;
+ assert(TileInfo.ActiveTile && "tile count without object?");
+ Diag(TileInfo.ActiveTile->getBeginLoc(), diag::note_acc_active_clause_here)
+ << OpenACCClauseKind::Tile;
+
+ // Remove the value so that we don't get cascading errors in the body. The
+ // caller RAII object will restore this.
+ TileInfo.CurTileCount = std::nullopt;
+ }
}
void SemaOpenACC::ActOnDoStmt(SourceLocation DoLoc) {
if (!getLangOpts().OpenACC)
return;
- if (!CollapseInfo.TopLevelLoopSeen)
+ if (!LoopInfo.TopLevelLoopSeen)
return;
if (CollapseInfo.CurCollapseCount && *CollapseInfo.CurCollapseCount > 0) {
- Diag(DoLoc, diag::err_acc_invalid_in_collapse_loop) << /*do loop*/ 2;
+ Diag(DoLoc, diag::err_acc_invalid_in_loop)
+ << /*do loop*/ 2 << OpenACCClauseKind::Collapse;
assert(CollapseInfo.ActiveCollapse && "Collapse count without object?");
Diag(CollapseInfo.ActiveCollapse->getBeginLoc(),
- diag::note_acc_collapse_clause_here);
+ diag::note_acc_active_clause_here)
+ << OpenACCClauseKind::Collapse;
// Remove the value so that we don't get cascading errors in the body. The
// caller RAII object will restore this.
CollapseInfo.CurCollapseCount = std::nullopt;
}
+
+ if (TileInfo.CurTileCount && *TileInfo.CurTileCount > 0) {
+ Diag(DoLoc, diag::err_acc_invalid_in_loop)
+ << /*do loop*/ 2 << OpenACCClauseKind::Tile;
+ assert(TileInfo.ActiveTile && "tile count without object?");
+ Diag(TileInfo.ActiveTile->getBeginLoc(), diag::note_acc_active_clause_here)
+ << OpenACCClauseKind::Tile;
+
+ // Remove the value so that we don't get cascading errors in the body. The
+ // caller RAII object will restore this.
+ TileInfo.CurTileCount = std::nullopt;
+ }
}
void SemaOpenACC::ActOnForStmtBegin(SourceLocation ForLoc) {
@@ -1810,7 +1860,7 @@ void SemaOpenACC::ActOnForStmtBegin(SourceLocation ForLoc) {
return;
// Enable the while/do-while checking.
- CollapseInfo.TopLevelLoopSeen = true;
+ LoopInfo.TopLevelLoopSeen = true;
if (CollapseInfo.CurCollapseCount && *CollapseInfo.CurCollapseCount > 0) {
@@ -1819,11 +1869,12 @@ void SemaOpenACC::ActOnForStmtBegin(SourceLocation ForLoc) {
// or loop nest.
// This checks for more than 1 loop at the current level, the
// 'depth'-satisifed checking manages the 'not zero' case.
- if (CollapseInfo.CurLevelHasLoopAlready) {
- Diag(ForLoc, diag::err_acc_collapse_multiple_loops);
+ if (LoopInfo.CurLevelHasLoopAlready) {
+ Diag(ForLoc, diag::err_acc_clause_multiple_loops) << /*Collapse*/ 0;
assert(CollapseInfo.ActiveCollapse && "No collapse object?");
Diag(CollapseInfo.ActiveCollapse->getBeginLoc(),
- diag::note_acc_collapse_clause_here);
+ diag::note_acc_active_clause_here)
+ << OpenACCClauseKind::Collapse;
} else {
--(*CollapseInfo.CurCollapseCount);
@@ -1834,13 +1885,29 @@ void SemaOpenACC::ActOnForStmtBegin(SourceLocation ForLoc) {
}
}
+ if (TileInfo.CurTileCount && *TileInfo.CurTileCount > 0) {
+ if (LoopInfo.CurLevelHasLoopAlready) {
+ Diag(ForLoc, diag::err_acc_clause_multiple_loops) << /*Tile*/ 1;
+ assert(TileInfo.ActiveTile && "No tile object?");
+ Diag(TileInfo.ActiveTile->getBeginLoc(),
+ diag::note_acc_active_clause_here)
+ << OpenACCClauseKind::Tile;
+ } else {
+ --(*TileInfo.CurTileCount);
+ // Once we've hit zero here, we know we have deep enough 'for' loops to
+ // get to the bottom.
+ if (*TileInfo.CurTileCount == 0)
+ TileInfo.TileDepthSatisfied = true;
+ }
+ }
+
// Set this to 'false' for the body of this loop, so that the next level
// checks independently.
- CollapseInfo.CurLevelHasLoopAlready = false;
+ LoopInfo.CurLevelHasLoopAlready = false;
}
namespace {
-SourceLocation FindInterveningCodeInCollapseLoop(const Stmt *CurStmt) {
+SourceLocation FindInterveningCodeInLoop(const Stmt *CurStmt) {
// We should diagnose on anything except `CompoundStmt`, `NullStmt`,
// `ForStmt`, `CXXForRangeStmt`, since those are legal, and `WhileStmt` and
// `DoStmt`, as those are caught as a violation elsewhere.
@@ -1858,8 +1925,7 @@ SourceLocation FindInterveningCodeInCollapseLoop(const Stmt *CurStmt) {
// of compound statements, as long as there isn't any code inside.
if (const auto *CS = dyn_cast<CompoundStmt>(CurStmt)) {
for (const auto *ChildStmt : CS->children()) {
- SourceLocation ChildStmtLoc =
- FindInterveningCodeInCollapseLoop(ChildStmt);
+ SourceLocation ChildStmtLoc = FindInterveningCodeInLoop(ChildStmt);
if (ChildStmtLoc.isValid())
return ChildStmtLoc;
}
@@ -1874,24 +1940,33 @@ void SemaOpenACC::ActOnForStmtEnd(SourceLocation ForLoc, StmtResult Body) {
if (!getLangOpts().OpenACC)
return;
// Set this to 'true' so if we find another one at this level we can diagnose.
- CollapseInfo.CurLevelHasLoopAlready = true;
+ LoopInfo.CurLevelHasLoopAlready = true;
if (!Body.isUsable())
return;
- if (CollapseInfo.CurCollapseCount && *CollapseInfo.CurCollapseCount > 0 &&
- !CollapseInfo.ActiveCollapse->hasForce()) {
- // OpenACC 3.3: 2.9.1
- // If the 'force' modifier does not appear, then the associated loops must
- // be tightly nested. If the force modifier appears, then any intervening
- // code may be executed multiple times as needed to perform the collapse.
+ bool IsActiveCollapse = CollapseInfo.CurCollapseCount &&
+ *CollapseInfo.CurCollapseCount > 0 &&
+ !CollapseInfo.ActiveCollapse->hasForce();
+ bool IsActiveTile = TileInfo.CurTileCount && *TileInfo.CurTileCount > 0;
- SourceLocation OtherStmtLoc = FindInterveningCodeInCollapseLoop(Body.get());
+ if (IsActiveCollapse || IsActiveTile) {
+ SourceLocation OtherStmtLoc = FindInterveningCodeInLoop(Body.get());
- if (OtherStmtLoc.isValid()) {
- Diag(OtherStmtLoc, diag::err_acc_collapse_intervening_code);
+ if (OtherStmtLoc.isValid() && IsActiveCollapse) {
+ Diag(OtherStmtLoc, diag::err_acc_intervening_code)
+ << OpenACCClauseKind::Collapse;
Diag(CollapseInfo.ActiveCollapse->getBeginLoc(),
- diag::note_acc_collapse_clause_here);
+ diag::note_acc_active_clause_here)
+ << OpenACCClauseKind::Collapse;
+ }
+
+ if (OtherStmtLoc.isValid() && IsActiveTile) {
+ Diag(OtherStmtLoc, diag::err_acc_intervening_code)
+ << OpenACCClauseKind::Tile;
+ Diag(TileInfo.ActiveTile->getBeginLoc(),
+ diag::note_acc_active_clause_here)
+ << OpenACCClauseKind::Tile;
}
}
}
@@ -1907,11 +1982,19 @@ bool SemaOpenACC::ActOnStartStmtDirective(OpenACCDirectiveKind K,
//
// ALL constructs are ill-formed if there is an active 'collapse'
if (CollapseInfo.CurCollapseCount && *CollapseInfo.CurCollapseCount > 0) {
- Diag(StartLoc, diag::err_acc_invalid_in_collapse_loop)
- << /*OpenACC Construct*/ 0 << K;
+ Diag(StartLoc, diag::err_acc_invalid_in_loop)
+ << /*OpenACC Construct*/ 0 << OpenACCClauseKind::Collapse << K;
assert(CollapseInfo.ActiveCollapse && "Collapse count without object?");
Diag(CollapseInfo.ActiveCollapse->getBeginLoc(),
- diag::note_acc_collapse_clause_here);
+ diag::note_acc_active_clause_here)
+ << OpenACCClauseKind::Collapse;
+ }
+ if (TileInfo.CurTileCount && *TileInfo.CurTileCount > 0) {
+ Diag(StartLoc, diag::err_acc_invalid_in_loop)
+ << /*OpenACC Construct*/ 0 << OpenACCClauseKind::Tile << K;
+ assert(TileInfo.ActiveTile && "Tile count without object?");
+ Diag(TileInfo.ActiveTile->getBeginLoc(), diag::note_acc_active_clause_here)
+ << OpenACCClauseKind::Tile;
}
return diagnoseConstructAppertainment(*this, K, StartLoc, /*IsStmt=*/true);
@@ -1986,11 +2069,24 @@ StmtResult SemaOpenACC::ActOnAssociatedStmt(SourceLocation DirectiveLoc,
return StmtError();
}
- if (!CollapseInfo.CollapseDepthSatisfied) {
- Diag(DirectiveLoc, diag::err_acc_collapse_insufficient_loops);
- assert(CollapseInfo.ActiveCollapse && "Collapse count without object?");
- Diag(CollapseInfo.ActiveCollapse->getBeginLoc(),
- diag::note_acc_collapse_clause_here);
+ if (!CollapseInfo.CollapseDepthSatisfied || !TileInfo.TileDepthSatisfied) {
+ if (!CollapseInfo.CollapseDepthSatisfied) {
+ Diag(DirectiveLoc, diag::err_acc_insufficient_loops)
+ << OpenACCClauseKind::Collapse;
+ assert(CollapseInfo.ActiveCollapse && "Collapse count without object?");
+ Diag(CollapseInfo.ActiveCollapse->getBeginLoc(),
+ diag::note_acc_active_clause_here)
+ << OpenACCClauseKind::Collapse;
+ }
+
+ if (!TileInfo.TileDepthSatisfied) {
+ Diag(DirectiveLoc, diag::err_acc_insufficient_loops)
+ << OpenACCClauseKind::Tile;
+ assert(TileInfo.ActiveTile && "Collapse count without object?");
+ Diag(TileInfo.ActiveTile->getBeginLoc(),
+ diag::note_acc_active_clause_here)
+ << OpenACCClauseKind::Tile;
+ }
return StmtError();
}
diff --git a/clang/test/ParserOpenACC/parse-clauses.c b/clang/test/ParserOpenACC/parse-clauses.c
index a752b2ebd18b6a..6c382379a8a7ea 100644
--- a/clang/test/ParserOpenACC/parse-clauses.c
...
[truncated]
|
the 'tile' clause requires that it be followed by N (where N is the
number of size expressions) 'tightly nested loops'. This means the
same as it does in 'collapse', so much of the implementation is simliar/shared with that.