blob: 06d9c50ecc6eb4467a0fee3ba3f3c9afac477489 [file] [edit]
//===-- lib/Semantics/compute-offsets.cpp -----------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "compute-offsets.h"
#include "flang/Evaluate/fold-designator.h"
#include "flang/Evaluate/fold.h"
#include "flang/Evaluate/shape.h"
#include "flang/Evaluate/type.h"
#include "flang/Runtime/descriptor-consts.h"
#include "flang/Semantics/scope.h"
#include "flang/Semantics/semantics.h"
#include "flang/Semantics/symbol.h"
#include "flang/Semantics/tools.h"
#include "flang/Semantics/type.h"
#include "llvm/TargetParser/Host.h"
#include "llvm/TargetParser/Triple.h"
#include <algorithm>
#include <cstdint>
#include <limits>
#include <vector>
namespace Fortran::semantics {
// Generated IR represents storage sizes and offsets as signed 64-bit integers.
static_assert(sizeof(std::size_t) >= sizeof(std::int64_t),
"byte sizes and offsets are accumulated in std::size_t and must not be "
"narrowed");
static constexpr std::size_t maxStorageSizeInBytes{
static_cast<std::size_t>(std::numeric_limits<std::int64_t>::max())};
static bool IsTooBig(std::size_t bytes) {
return bytes > maxStorageSizeInBytes;
}
// Add sizes while tracking whether the signed 64-bit limit was exceeded.
static std::size_t AddSizes(std::size_t x, std::size_t y, bool &tooBig) {
tooBig |= IsTooBig(x) || IsTooBig(y) || x > maxStorageSizeInBytes - y;
return x + y;
}
// A folded extent is (ub-lb+1) evaluated with signed 64-bit arithmetic, so it
// comes out nonpositive both for an empty dimension and for one that wrapped
// around, as in a(0:huge(0_8)). Tell those apart with the declared bounds.
static bool IsEmptyDimension(const Symbol &symbol, int dimension) {
if (const auto *object{symbol.detailsIf<ObjectEntityDetails>()}) {
const ArraySpec &shape{object->shape()};
if (dimension < shape.Rank()) {
auto lb{evaluate::ToInt64(shape[dimension].lbound().GetExplicit())};
auto ub{evaluate::ToInt64(shape[dimension].ubound().GetExplicit())};
if (lb && ub) {
return *ub < *lb;
}
}
}
return true; // no constant bounds to contradict the folded extent
}
class ComputeOffsetsHelper {
public:
ComputeOffsetsHelper(SemanticsContext &context) : context_{context} {}
void Compute(Scope &);
private:
struct SizeAndAlignment {
SizeAndAlignment() {}
SizeAndAlignment(std::size_t bytes) : size{bytes}, alignment{bytes} {}
SizeAndAlignment(std::size_t bytes, std::size_t align)
: size{bytes}, alignment{align} {}
SizeAndAlignment(std::size_t bytes, std::size_t align, const Symbol *tooBig)
: size{bytes}, alignment{align}, oversized{tooBig} {}
std::size_t size{0};
std::size_t alignment{0};
// Null unless the size exceeds maxStorageSizeInBytes, in which case the
// size is clamped to it and this is the symbol to blame in a diagnostic:
// for an EQUIVALENCE storage sequence, the first member found not to fit.
const Symbol *oversized{nullptr};
};
struct SymbolAndOffset {
SymbolAndOffset(Symbol &s, std::size_t off, const EquivalenceObject &obj)
: symbol{s}, offset{off}, object{&obj} {}
SymbolAndOffset(const SymbolAndOffset &) = default;
MutableSymbolRef symbol;
std::size_t offset;
const EquivalenceObject *object;
bool offsetOverflow{false};
};
void DoCommonBlock(Symbol &);
void DoEquivalenceBlockBase(Symbol &, SizeAndAlignment &);
void DoEquivalenceSet(const EquivalenceSet &);
SymbolAndOffset Resolve(const SymbolAndOffset &);
std::size_t ComputeOffset(const EquivalenceObject &);
// Returns amount of padding that was needed for alignment
std::size_t DoSymbol(
Symbol &, std::optional<const size_t> newAlign = std::nullopt);
SizeAndAlignment GetSizeAndAlignment(const Symbol &, bool entire);
std::size_t Align(std::size_t, std::size_t);
std::optional<size_t> CompAlignment(const Symbol &);
std::optional<size_t> HasSpecialAlign(const Symbol &, Scope &);
SemanticsContext &context_;
std::size_t offset_{0};
std::size_t alignment_{1};
bool sizeOverflow_{false};
// symbol -> symbol+offset that determines its location, from EQUIVALENCE
std::map<MutableSymbolRef, SymbolAndOffset, SymbolAddressCompare> dependents_;
// base symbol -> SizeAndAlignment for each distinct EQUIVALENCE block
std::map<MutableSymbolRef, SizeAndAlignment, SymbolAddressCompare>
equivalenceBlock_;
};
// This function is only called if the target platform is AIX.
static bool isReal8OrLarger(const Fortran::semantics::DeclTypeSpec *type) {
return ((type->IsNumeric(common::TypeCategory::Real) ||
type->IsNumeric(common::TypeCategory::Complex)) &&
evaluate::ToInt64(type->numericTypeSpec().kind()) > 4);
}
// This function is only called if the target platform is AIX.
// It determines the alignment of a component. If the component is a derived
// type, the alignment is computed accordingly.
std::optional<size_t> ComputeOffsetsHelper::CompAlignment(const Symbol &sym) {
size_t max_align{0};
constexpr size_t fourByteAlign{4};
bool contain_double{false};
auto derivedTypeSpec{sym.GetType()->AsDerived()};
DirectComponentIterator directs{*derivedTypeSpec};
for (auto it{directs.begin()}; it != directs.end(); ++it) {
auto type{it->GetType()};
auto s{GetSizeAndAlignment(*it, true)};
if (isReal8OrLarger(type)) {
max_align = std::max(max_align, fourByteAlign);
contain_double = true;
} else if (type->AsDerived()) {
if (const auto newAlgin{CompAlignment(*it)}) {
max_align = std::max(max_align, s.alignment);
} else {
return std::nullopt;
}
} else {
max_align = std::max(max_align, s.alignment);
}
}
if (contain_double) {
return max_align;
} else {
return std::nullopt;
}
}
// This function is only called if the target platform is AIX.
// Special alignment is needed only if it is a bind(c) derived type
// and contain real type components that have larger than 4 bytes.
std::optional<size_t> ComputeOffsetsHelper::HasSpecialAlign(
const Symbol &sym, Scope &scope) {
// On AIX, if the component that is not the first component and is
// a float of 8 bytes or larger, it has the 4-byte alignment.
// Only set the special alignment for bind(c) derived type on that platform.
if (const auto type{sym.GetType()}) {
auto &symOwner{sym.owner()};
if (symOwner.symbol() && symOwner.IsDerivedType() &&
symOwner.symbol()->attrs().HasAny({semantics::Attr::BIND_C}) &&
&sym != &(*scope.GetSymbols().front())) {
if (isReal8OrLarger(type)) {
return 4UL;
} else if (type->AsDerived()) {
return CompAlignment(sym);
}
}
}
return std::nullopt;
}
void ComputeOffsetsHelper::Compute(Scope &scope) {
for (Scope &child : scope.children()) {
ComputeOffsets(context_, child);
}
if (scope.symbol() && scope.IsDerivedTypeWithKindParameter()) {
return; // only process instantiations of kind parameterized derived types
}
if (scope.alignment().has_value()) {
return; // prevent infinite recursion in error cases
}
scope.SetAlignment(0);
// Build dependents_ from equivalences: symbol -> symbol+offset
for (const EquivalenceSet &set : scope.equivalenceSets()) {
DoEquivalenceSet(set);
}
// Compute a base symbol and overall block size for each
// disjoint EQUIVALENCE storage sequence.
for (auto &[symbol, dep] : dependents_) {
dep = Resolve(dep);
CHECK(symbol->size() == 0);
auto symInfo{GetSizeAndAlignment(*symbol, true)};
symbol->set_size(symInfo.size);
Symbol &base{*dep.symbol};
auto iter{equivalenceBlock_.find(base)};
bool blockOverflow{dep.offsetOverflow || symInfo.oversized};
std::size_t minBlockSize{AddSizes(dep.offset, symInfo.size, blockOverflow)};
const Symbol *oversized{
blockOverflow || IsTooBig(minBlockSize) ? &*symbol : nullptr};
if (iter == equivalenceBlock_.end()) {
equivalenceBlock_.emplace(
base, SizeAndAlignment{minBlockSize, symInfo.alignment, oversized});
} else {
SizeAndAlignment &blockInfo{iter->second};
blockInfo.size = std::max(blockInfo.size, minBlockSize);
blockInfo.alignment = std::max(blockInfo.alignment, symInfo.alignment);
if (!blockInfo.oversized) {
blockInfo.oversized = oversized;
}
}
}
// Complete each EQUIVALENCE block with its base object, and assign offsets
// for non-COMMON blocks.
for (auto &[symbol, blockInfo] : equivalenceBlock_) {
// The base does not appear in dependents_.
SizeAndAlignment baseInfo{GetSizeAndAlignment(*symbol, true)};
blockInfo.size = std::max(blockInfo.size, baseInfo.size);
blockInfo.alignment = std::max(blockInfo.alignment, baseInfo.alignment);
if (!blockInfo.oversized &&
(baseInfo.oversized || IsTooBig(baseInfo.size))) {
blockInfo.oversized = &*symbol;
}
if (!FindCommonBlockContaining(*symbol)) {
DoSymbol(*symbol);
DoEquivalenceBlockBase(*symbol, blockInfo);
// Each EQUIVALENCE block is lowered as one aggregate. Blame a member
// that does not fit rather than the base object, whose selection in
// DoEquivalenceSet is a layout decision that is invisible to the user.
if (blockInfo.oversized || IsTooBig(blockInfo.size)) {
const Symbol &blamed{
blockInfo.oversized ? *blockInfo.oversized : *symbol};
context_.Say(blamed.name(),
"The size of the storage sequence created by EQUIVALENCE with '%s' exceeds the maximum supported size of %zu bytes"_err_en_US,
blamed.name(), maxStorageSizeInBytes);
}
offset_ = std::max(offset_, symbol->offset() + blockInfo.size);
}
}
// Process remaining non-COMMON symbols; this is all of them if there
// was no use of EQUIVALENCE in the scope.
for (auto &symbol : scope.GetSymbols()) {
if (!FindCommonBlockContaining(*symbol) &&
dependents_.find(symbol) == dependents_.end() &&
equivalenceBlock_.find(symbol) == equivalenceBlock_.end()) {
std::optional<size_t> newAlign{std::nullopt};
// Handle special alignment requirement for AIX
auto triple{llvm::Triple(
llvm::Triple::normalize(llvm::sys::getDefaultTargetTriple()))};
if (triple.getOS() == llvm::Triple::OSType::AIX) {
newAlign = HasSpecialAlign(*symbol, scope);
}
DoSymbol(*symbol, newAlign);
if (auto *generic{symbol->detailsIf<GenericDetails>()}) {
if (Symbol * specific{generic->specific()};
specific && !FindCommonBlockContaining(*specific)) {
// might be a shadowed procedure pointer
DoSymbol(*specific);
}
}
}
}
// Ensure that the size is a multiple of the alignment
offset_ = Align(offset_, alignment_);
sizeOverflow_ |= IsTooBig(offset_);
// Only derived-type scope sizes are materialized in generated IR.
if (sizeOverflow_ && scope.IsDerivedType() && scope.symbol()) {
context_.Say(scope.symbol()->name(),
"The size of derived type '%s' exceeds the maximum supported size of %zu bytes"_err_en_US,
scope.symbol()->name(), maxStorageSizeInBytes);
}
scope.set_size(offset_);
scope.SetAlignment(alignment_);
// Assign offsets in COMMON blocks, unless this scope is a BLOCK construct,
// where COMMON blocks are illegal (C1107 and C1108).
if (scope.kind() != Scope::Kind::BlockConstruct) {
for (auto &pair : scope.commonBlocks()) {
DoCommonBlock(*pair.second);
}
}
for (auto &[symbol, dep] : dependents_) {
symbol->set_offset(dep.symbol->offset() + dep.offset);
if (const auto *block{FindCommonBlockContaining(*dep.symbol)}) {
if (auto *object{symbol->detailsIf<ObjectEntityDetails>()}) {
object->set_commonBlock(*block);
}
}
}
}
auto ComputeOffsetsHelper::Resolve(const SymbolAndOffset &dep)
-> SymbolAndOffset {
auto it{dependents_.find(*dep.symbol)};
if (it == dependents_.end()) {
return dep;
} else {
SymbolAndOffset result{Resolve(it->second)};
// Preserve overflow while resolving EQUIVALENCE chains, both the overflow
// already recorded for the offset being resolved and any that appears when
// accumulating it.
result.offsetOverflow |= dep.offsetOverflow;
result.offset = AddSizes(result.offset, dep.offset, result.offsetOverflow);
result.object = dep.object;
return result;
}
}
void ComputeOffsetsHelper::DoCommonBlock(Symbol &commonBlock) {
auto &details{commonBlock.get<CommonBlockDetails>()};
offset_ = 0;
alignment_ = 0;
sizeOverflow_ = false;
std::size_t minSize{0};
std::size_t minAlignment{0};
UnorderedSymbolSet previous;
for (auto object : details.objects()) {
// Allow for host association when the common block is
// OpenMP firstprivate.
Symbol &symbol{object->GetUltimate()};
auto errorSite{
commonBlock.name().empty() ? symbol.name() : commonBlock.name()};
if (std::size_t padding{DoSymbol(symbol.GetUltimate())}) {
context_.Warn(common::UsageWarning::CommonBlockPadding, errorSite,
"COMMON block /%s/ requires %zd bytes of padding before '%s' for alignment"_port_en_US,
commonBlock.name(), padding, symbol.name());
}
previous.emplace(symbol);
auto eqIter{equivalenceBlock_.end()};
auto iter{dependents_.find(symbol)};
if (iter == dependents_.end()) {
eqIter = equivalenceBlock_.find(symbol);
if (eqIter != equivalenceBlock_.end()) {
DoEquivalenceBlockBase(symbol, eqIter->second);
}
} else {
SymbolAndOffset &dep{iter->second};
Symbol &base{*dep.symbol};
if (const auto *baseBlock{FindCommonBlockContaining(base)}) {
if (baseBlock == &commonBlock) {
if (previous.find(SymbolRef{base}) == previous.end() ||
base.offset() != symbol.offset() - dep.offset) {
context_.Say(errorSite,
"'%s' is storage associated with '%s' by EQUIVALENCE elsewhere in COMMON block /%s/"_err_en_US,
symbol.name(), base.name(), commonBlock.name());
}
} else { // F'2023 8.10.3 p1
context_.Say(errorSite,
"'%s' in COMMON block /%s/ must not be storage associated with '%s' in COMMON block /%s/ by EQUIVALENCE"_err_en_US,
symbol.name(), commonBlock.name(), base.name(),
baseBlock->name());
}
} else if (dep.offset > symbol.offset()) { // 8.10.3(3)
context_.Say(errorSite,
"'%s' cannot backward-extend COMMON block /%s/ via EQUIVALENCE with '%s'"_err_en_US,
symbol.name(), commonBlock.name(), base.name());
} else {
eqIter = equivalenceBlock_.find(base);
base.get<ObjectEntityDetails>().set_commonBlock(commonBlock);
base.set_offset(symbol.offset() - dep.offset);
previous.emplace(base);
}
}
// Get full extent of any EQUIVALENCE block into size of COMMON ( see
// 8.10.2.2 point 1 (2))
if (eqIter != equivalenceBlock_.end()) {
SizeAndAlignment &blockInfo{eqIter->second};
sizeOverflow_ |= blockInfo.oversized != nullptr;
std::size_t blockEnd{
AddSizes(eqIter->first->offset(), blockInfo.size, sizeOverflow_)};
minSize = std::max(minSize, std::max(offset_, blockEnd));
minAlignment = std::max(minAlignment, blockInfo.alignment);
}
}
std::size_t size{std::max(minSize, offset_)};
if (sizeOverflow_) {
context_.Say(details.sourceLocation(),
"The size of COMMON block /%s/ exceeds the maximum supported size of %zu bytes"_err_en_US,
commonBlock.name(), maxStorageSizeInBytes);
}
commonBlock.set_size(size);
details.set_alignment(std::max(minAlignment, alignment_));
context_.MapCommonBlockAndCheckConflicts(commonBlock);
}
void ComputeOffsetsHelper::DoEquivalenceBlockBase(
Symbol &symbol, SizeAndAlignment &blockInfo) {
if (symbol.size() > blockInfo.size) {
blockInfo.size = symbol.size();
}
}
void ComputeOffsetsHelper::DoEquivalenceSet(const EquivalenceSet &set) {
std::vector<SymbolAndOffset> symbolOffsets;
std::optional<std::size_t> representative;
for (const EquivalenceObject &object : set) {
std::size_t offset{ComputeOffset(object)};
SymbolAndOffset resolved{
Resolve(SymbolAndOffset{object.symbol, offset, object})};
symbolOffsets.push_back(resolved);
if (!representative ||
resolved.offset >= symbolOffsets[*representative].offset) {
// The equivalenced object with the largest offset from its resolved
// symbol will be the representative of this set, since the offsets
// of the other objects will be positive relative to it.
representative = symbolOffsets.size() - 1;
}
}
CHECK(representative);
const SymbolAndOffset &base{symbolOffsets[*representative]};
for (const auto &[symbol, offset, object, offsetOverflow] : symbolOffsets) {
if (symbol == base.symbol) {
if (offset != base.offset) {
auto x{evaluate::OffsetToDesignator(
context_.foldingContext(), *symbol, base.offset, 1)};
auto y{evaluate::OffsetToDesignator(
context_.foldingContext(), *symbol, offset, 1)};
if (x && y) {
context_
.Say(base.object->source,
"'%s' and '%s' cannot have the same first storage unit"_err_en_US,
x->AsFortran(), y->AsFortran())
.Attach(object->source, "Incompatible reference to '%s'"_en_US,
y->AsFortran());
} else { // error recovery
context_
.Say(base.object->source,
"'%s' (offset %zd bytes and %zd bytes) cannot have the same first storage unit"_err_en_US,
symbol->name(), base.offset, offset)
.Attach(object->source,
"Incompatible reference to '%s' offset %zd bytes"_en_US,
symbol->name(), offset);
}
}
} else {
SymbolAndOffset dependent{*base.symbol, base.offset - offset, *object};
dependent.offsetOverflow = base.offsetOverflow || offsetOverflow;
dependents_.emplace(*symbol, dependent);
}
}
}
// Offset of this equivalence object from the start of its variable.
std::size_t ComputeOffsetsHelper::ComputeOffset(
const EquivalenceObject &object) {
std::size_t offset{0};
if (!object.subscripts.empty()) {
if (const auto *details{object.symbol.detailsIf<ObjectEntityDetails>()}) {
const ArraySpec &shape{details->shape()};
auto lbound{[&](std::size_t i) {
return *ToInt64(shape[i].lbound().GetExplicit());
}};
auto ubound{[&](std::size_t i) {
return *ToInt64(shape[i].ubound().GetExplicit());
}};
for (std::size_t i{object.subscripts.size() - 1};;) {
offset += object.subscripts[i] - lbound(i);
if (i == 0) {
break;
}
--i;
offset *= ubound(i) - lbound(i) + 1;
}
}
}
auto result{offset * GetSizeAndAlignment(object.symbol, false).size};
if (object.substringStart) {
int kind{context_.defaultKinds().GetDefaultKind(TypeCategory::Character)};
if (const DeclTypeSpec * type{object.symbol.GetType()}) {
if (const IntrinsicTypeSpec * intrinsic{type->AsIntrinsic()}) {
kind = ToInt64(intrinsic->kind()).value_or(kind);
}
}
result += kind * (*object.substringStart - 1);
}
return result;
}
std::size_t ComputeOffsetsHelper::DoSymbol(
Symbol &symbol, std::optional<const size_t> newAlign) {
if (!symbol.has<ObjectEntityDetails>() && !symbol.has<ProcEntityDetails>()) {
return 0;
}
SizeAndAlignment s{GetSizeAndAlignment(symbol, true)};
// Oversized standalone objects are left to object emission.
sizeOverflow_ |= s.oversized != nullptr;
if (s.size == 0) {
// Zero-size symbols (e.g. CHARACTER*0) still occupy their sequential
// position in a COMMON block or derived-type sequence. Record the current
// offset so that LOC() and storage-association checks see the correct
// address rather than always returning the block base (offset 0).
symbol.set_size(0);
symbol.set_offset(offset_);
return 0;
}
std::size_t previousOffset{offset_};
size_t alignVal{newAlign.value_or(s.alignment)};
offset_ = Align(offset_, alignVal);
sizeOverflow_ |= IsTooBig(offset_);
std::size_t padding{offset_ - previousOffset};
symbol.set_size(s.size);
symbol.set_offset(offset_);
offset_ = AddSizes(offset_, s.size, sizeOverflow_);
alignment_ = std::max(alignment_, alignVal);
return padding;
}
auto ComputeOffsetsHelper::GetSizeAndAlignment(
const Symbol &symbol, bool entire) -> SizeAndAlignment {
auto &targetCharacteristics{context_.targetCharacteristics()};
if (IsDescriptor(symbol)) {
auto dyType{evaluate::DynamicType::From(symbol)};
const auto *derived{evaluate::GetDerivedTypeSpec(dyType)};
int lenParams{derived ? CountLenParameters(*derived) : 0};
bool needAddendum{derived || (dyType && dyType->IsUnlimitedPolymorphic())};
// FIXME: Get descriptor size from targetCharacteristics instead
// overapproximation
std::size_t size{runtime::MaxDescriptorSizeInBytes(
symbol.Rank(), needAddendum, lenParams)};
return {size, targetCharacteristics.descriptorAlignment()};
}
if (IsProcedurePointer(symbol)) {
return {targetCharacteristics.procedurePointerByteSize(),
targetCharacteristics.procedurePointerAlignment()};
}
if (IsProcedure(symbol)) {
return {};
}
auto &foldingContext{context_.foldingContext()};
if (auto chars{evaluate::characteristics::TypeAndShape::Characterize(
symbol, foldingContext)}) {
std::size_t alignment{chars->type().GetAlignment(targetCharacteristics)};
// Avoid folded products, which can wrap in signed 64-bit arithmetic.
bool aligned{!entire || chars->Rank() > 0};
std::size_t size;
if (chars->type().category() == TypeCategory::Character && chars->LEN()) {
auto length{ToInt64(*chars->LEN())};
if (!length) {
return {};
}
auto bytesPerCharacter{
static_cast<std::size_t>(targetCharacteristics.GetByteSize(
TypeCategory::Character, chars->type().kind()))};
if (*length < 0 ||
static_cast<std::size_t>(*length) >
maxStorageSizeInBytes / bytesPerCharacter) {
return {maxStorageSizeInBytes, alignment, &symbol};
}
size = static_cast<std::size_t>(*length) * bytesPerCharacter;
} else {
auto elementSize{
ToInt64(chars->MeasureElementSizeInBytes(foldingContext, aligned))};
if (!elementSize) {
return {};
}
if (*elementSize < 0) {
return {maxStorageSizeInBytes, alignment, &symbol};
}
size = static_cast<std::size_t>(*elementSize);
}
if (!entire) { // element size only
return {size, alignment};
}
if (auto extents{
evaluate::AsConstantExtents(foldingContext, chars->shape())}) {
if (size == 0) { // zero-sized elements occupy no storage
return {0, alignment};
}
for (int dimension{0}; dimension < static_cast<int>(extents->size());
++dimension) {
if ((*extents)[dimension] <= 0) {
if (!IsEmptyDimension(symbol, dimension)) {
return {maxStorageSizeInBytes, alignment, &symbol};
}
return {0, alignment}; // a zero-sized array occupies no storage
}
}
for (ConstantSubscript extent : *extents) {
auto n{static_cast<std::size_t>(extent)};
if (size > maxStorageSizeInBytes / n) {
return {maxStorageSizeInBytes, alignment, &symbol};
}
size *= n;
}
return {size, alignment};
}
}
return {};
}
// Align a size to its natural alignment, up to maxAlignment.
std::size_t ComputeOffsetsHelper::Align(std::size_t x, std::size_t alignment) {
alignment =
std::min(alignment, context_.targetCharacteristics().maxAlignment());
return (x + alignment - 1) & -alignment;
}
void ComputeOffsets(SemanticsContext &context, Scope &scope) {
ComputeOffsetsHelper{context}.Compute(scope);
}
} // namespace Fortran::semantics