Files
carbon-lang/toolchain/check/scope_stack.h
T
Chandler Carruth ae4be1be14 Use inline small storage for small SemIR ID sets in toolchain (#7796)
Apply SmallSize = 16 to frequent identifier and instruction/function
sets in ScopeStack, Class, FacetType, and SpecificCoalescer to avoid
dynamic heap allocations on small scopes. Also defer dest_field_names
set allocation in struct conversion and provide default KeyContext for
SetBase. This was found by inspection, but does seem to be a clear 1.5%
win on overall compile time.

```
Ran baseline and experiment 10 times on 128 x 2450 MHz CPUs
CPU caches:
  L1 Data 32Ki
  L1 Instruction 32Ki
  L2 Unified 512Ki
  L3 Unified 32Mi
Load avg: 1.2041 1.16895 2.86133
Computing statistically significant deltas only wherethe P-value < 𝛂 of 0.05
Metric key:
   BenchmarkName... 👍 <delta>    p=<U-test P-value>
          baseline:    <median> ± <% at 95th conf>
        experiment:    <median> ± <% at 95th conf>

 Benchmark                                                      ┃          CPU Time          ┃           CYCLES           ┃       INSTRUCTIONS        ┃          Lines
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━
 BM_CompileApiFileDenseDecls<Lang::Carbon, Phase::Check>/256... │ 👍  -1.768%      p=0.0346  │ 👍  -1.683%      p=0.0346  │ 👍   0.160%    p=0.000428 │ ~
                                                      baseline: │     49.11  ms  ±   1.363%  │    155.6   M   ±   1.875%  │    293.9   M ±   0.015%   │     4.093 k ±   1.360%
                                                    experiment: │     48.24  ms  ±   2.213%  │    153     M   ±   2.465%  │    293.4   M ±   0.081%   │     4.166 k ±   2.165%
                                                                │                            │                            │                           │
 BM_CompileApiFileDenseDecls<Lang::Carbon, Phase::Check>/1024.. │      ??          p=0.159   │      ??          p=0.067   │ 👍   0.153%    p=0.000249 │ ~
                                                      baseline: │     50.61  ms  ±   2.379%  │    160.7   M   ±   2.777%  │    309.7   M ±   0.015%   │    19.96  k ±   2.326%
                                                    experiment: │     50.32  ms  ±   0.971%  │    159.6   M   ±   0.787%  │    309.2   M ±   0.086%   │    20.07  k ±   0.980%
                                                                │                            │                            │                           │
 BM_CompileApiFileDenseDecls<Lang::Carbon, Phase::Check>/4096.. │ 👍  -1.593%      p=0.0112  │ 👍  -1.632%      p=0.00743 │ 👍   0.138%    p=0.000328 │ ~
                                                      baseline: │     58.58  ms  ±   1.673%  │    186.5   M   ±   1.487%  │    371.2   M ±   0.102%   │    70.96  k ±   1.645%
                                                    experiment: │     57.65  ms  ±   1.032%  │    183.5   M   ±   1.079%  │    370.7   M ±   0.091%   │    72.11  k ±   1.043%
                                                                │                            │                            │                           │
 BM_CompileApiFileDenseDecls<Lang::Carbon, Phase::Check>/16384. │ 👍  -1.695%      p=0.029   │ 👍  -1.823%      p=0.0411  │ 👍   0.221%    p=0.000428 │ ~
                                                      baseline: │     90.07  ms  ±   3.686%  │    287.1   M   ±   3.694%  │    618.9   M ±   0.106%   │   186.9   k ±   3.555%
                                                    experiment: │     88.55  ms  ±   1.891%  │    281.8   M   ±   2.142%  │    617.6   M ±   0.158%   │   190.1   k ±   1.856%
                                                                │                            │                            │                           │
 BM_CompileApiFileDenseDecls<Lang::Carbon, Phase::Check>/65536. │ 👍  -1.797%      p=0.0201  │ 👍  -1.762%      p=0.0201  │ 👍   0.222%    p=0.000328 │ ~
                                                      baseline: │    222.1   ms  ±   2.560%  │    711     M   ±   2.417%  │      1.613 G ±   0.158%   │   304.2   k ±   2.496%
                                                    experiment: │    218.1   ms  ±   1.950%  │    698.5   M   ±   2.010%  │      1.61  G ±   0.075%   │   309.7   k ±   1.989%
                                                                │                            │                            │                           │
 BM_CompileApiFileDenseDecls<Lang::Carbon, Phase::Check>/262144 │      ??          p=0.398   │      ??          p=0.36    │ 👍   0.166%    p=0.000931 │ ~
                                                      baseline: │    782.2   ms  ±   2.666%  │      2.502 G   ±   2.546%  │      5.596 G ±   0.038%   │   345.8   k ±   2.597%
                                                    experiment: │    780.6   ms  ±   4.249%  │      2.483 G   ±   4.691%  │      5.587 G ±   0.024%   │   346.5   k ±   4.075%
                                                                │                            │                            │                           │
```

Assisted-by: Antigravity with Gemini
2026-09-17 15:49:43 +00:00

496 lines
20 KiB
C++

// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#ifndef CARBON_TOOLCHAIN_CHECK_SCOPE_STACK_H_
#define CARBON_TOOLCHAIN_CHECK_SCOPE_STACK_H_
#include "common/array_stack.h"
#include "common/move_only.h"
#include "common/set.h"
#include "llvm/ADT/SmallVector.h"
#include "toolchain/check/full_pattern_stack.h"
#include "toolchain/check/lexical_lookup.h"
#include "toolchain/check/scope_index.h"
#include "toolchain/sem_ir/file.h"
#include "toolchain/sem_ir/ids.h"
namespace Carbon::Check {
class Context;
// A stack of lexical and semantic scopes that we are currently performing
// checking within.
class ScopeStack {
public:
explicit ScopeStack(Context& context);
// The kind of cleanup scope that is associated with a scope. A cleanup scope
// contains the destructor calls that are necessary to perform when leaving a
// scope.
enum class CleanupScopeKind : uint8_t {
// This scope is not used for runtime expression evaluation, so should only
// contain constants. Any cleanups introduced here can be discarded.
// TODO: Is this the behavior we want for such scopes? Should we check that
// the cleanups themselves are constant?
None,
// This scope is associated with a subexpression, not a full-expression, so
// its cleanup scope is inherited from the parent scope.
Inherited,
// This scope is associated with a statement, so it owns a cleanup scope,
// and its cleanups must be run or explicitly discarded when exiting this
// scope.
Owned,
};
// An index for the distance to the bottom of the cleanup stack.
struct CleanupScopeDepth : public IndexBase<CleanupScopeDepth> {
static constexpr llvm::StringLiteral Label = "cleanup_scope_depth";
using IndexBase::IndexBase;
};
// A scope in which `break` and `continue` can be used.
struct BreakContinueScope {
SemIR::InstBlockId break_target;
CleanupScopeDepth break_depth;
SemIR::InstBlockId continue_target;
CleanupScopeDepth continue_depth;
};
// A non-lexical scope in which unqualified lookup may be required.
struct NonLexicalScope {
// The index of the scope in the scope stack.
ScopeIndex scope_index;
// The corresponding name scope.
SemIR::NameScopeId name_scope_id;
// The corresponding specific.
SemIR::SpecificId specific_id;
};
// Information about a scope that has been temporarily removed from the stack.
// This type is large, so moves of this type should be avoided.
struct SuspendedScope;
// Pushes a scope for a declaration name's parameters.
auto PushForDeclName() -> void;
// Pushes a non-function entity scope. Functions must use
// `PushForFunctionBody` instead.
auto PushForEntity(SemIR::InstId scope_inst_id, SemIR::NameScopeId scope_id,
SemIR::SpecificId specific_id,
bool lexical_lookup_has_load_error = false) -> void;
// Pushes a scope which should be in the same region as the current scope.
// These can be in a function without breaking `return` scoping. For example,
// this is used by struct literals and code blocks.
auto PushForSameRegion(CleanupScopeKind cleanup_scope_kind =
CleanupScopeKind::Inherited) -> void;
// Pushes a function scope.
auto PushForFunctionBody(SemIR::InstId scope_inst_id) -> void;
// Pops the top scope from scope_stack_. Removes names from lexical_lookup_.
// If `check_unused` is set, checks and emits diagnostics for unused names.
auto Pop(bool check_unused = false) -> void;
// Pops the top scope from scope_stack_ if it contains no names.
auto PopIfEmpty(bool check_unused = false) -> void {
if (scope_stack_.back().num_names == 0) {
Pop(check_unused);
}
}
// Pops scopes until we return to the specified scope index.
auto PopTo(ScopeIndex index, bool check_unused = false) -> void;
// Returns the scope index associated with the current scope.
auto PeekIndex() const -> ScopeIndex { return Peek().index; }
// Returns the name scope associated with the current lexical scope, if any.
auto PeekNameScopeId() const -> SemIR::NameScopeId { return Peek().scope_id; }
// Returns the instruction associated with the current scope, or `None` if
// there is no such instruction, such as for a block scope.
auto PeekInstId() const -> SemIR::InstId { return Peek().scope_inst_id; }
// Returns the instruction associated with the parent scope, or `None` if
// there is no such instruction, such as for a block scope.
auto PeekParentInstId() const -> SemIR::InstId {
return Peek(1).scope_inst_id;
}
// Returns the specific associated with the innermost enclosing scope that is
// associated with a specific. This will generally be the self specific of the
// innermost enclosing generic, as there is no way to enter any other specific
// scope.
auto PeekSpecificId() const -> SemIR::SpecificId {
return Peek().specific_id;
}
// Returns true if the current scope is inside a function scope (either the
// scope itself, or a lexical scope), without an intervening entity scope.
auto IsInFunctionScope() const -> bool {
return !return_scope_stack_.empty() &&
!return_scope_stack_.back().nested_scope_index.has_value();
}
// Merges the innermost scope into its grandparent scope, and pops the
// now-empty scope. This is used when handling a `for` statement. Given:
//
// for (var a: i32 in MakeTempRange()) {
//
// we have an outer scope containing `var a: i32` and an inner scope
// containing `MakeTempRange()`, and we want them the other way around, so we
// create an extra enclosing scope in advance and merge the inner scope into
// it.
//
// Requires that no names were introduced in the innermost scope.
auto MergeTopScopeIntoGrandparentAndPop() -> void;
// Returns the current scope, if it is of the specified kind. Otherwise,
// returns nullopt.
template <typename InstT>
auto TryGetCurrentScopeAs() -> std::optional<InstT> {
auto inst_id = PeekInstId();
if (!inst_id.has_value()) {
return std::nullopt;
}
return sem_ir().insts().TryGetAs<InstT>(inst_id);
}
// Returns the current scope, assuming it is of the specified kind.
// Check-fails if there is no instruction for a current scope, or the scope is
// of a different kind.
template <typename InstT>
auto GetCurrentScopeAs() -> InstT {
auto inst_id = PeekInstId();
CARBON_CHECK(inst_id.has_value());
return sem_ir().insts().GetAs<InstT>(inst_id);
}
// If there is no `returned var` in scope, sets the given instruction to be
// the current `returned var` and returns an `None`. If there
// is already a `returned var`, returns it instead.
auto SetReturnedVarOrGetExisting(SemIR::InstId inst_id, SemIR::NameId name_id)
-> SemIR::InstId;
// Returns the `returned var` instruction that's currently in scope, or `None`
// if there isn't one.
auto GetReturnedVar() -> SemIR::InstId {
CARBON_CHECK(IsInFunctionScope(), "Handling return but not in a function");
return return_scope_stack_.back().returned_var;
}
// Returns the decl ID for the current return scope.
auto GetReturnScopeDeclId() -> SemIR::InstId {
CARBON_CHECK(IsInFunctionScope(), "Handling return but not in a function");
return return_scope_stack_.back().decl_id;
}
// Looks up the name `name_id` in the current scope and enclosing scopes, but
// do not look past `scope_index`. Returns the existing lookup result, if any.
// If `use_loc_id` is specified, the name is marked as used at that location.
auto LookupInLexicalScopesWithin(SemIR::NameId name_id,
ScopeIndex scope_index,
SemIR::LocId use_loc_id, bool is_reachable)
-> SemIR::InstId;
// Looks up the name `name_id` in the current scope and related lexical
// scopes. Returns the innermost lexical lookup result, if any, along with a
// list of non-lexical scopes in which lookup should also be performed,
// ordered from outermost to innermost. If `use_loc_id` is specified, the
// name is marked as used at that location.
auto LookupInLexicalScopes(SemIR::NameId name_id, SemIR::LocId use_loc_id,
bool is_reachable)
-> std::pair<SemIR::InstId, llvm::ArrayRef<NonLexicalScope>>;
// Looks up the name `name_id` in the current scope, or in `scope_index` if
// specified. Returns the existing instruction if the name is already declared
// in that scope or any unfinished scope within it, and otherwise adds the
// name with the value `target_id` and returns `None`. `is_decl_reachable`
// indicates whether the name was declared in a reachable position.
auto LookupOrAddName(SemIR::NameId name_id, SemIR::InstId target_id,
ScopeIndex scope_index = ScopeIndex::None,
bool is_decl_reachable = true) -> SemIR::InstId;
// Prepares to add a compile-time binding in the current scope, and returns
// its index. The added binding must then be pushed using
// `PushCompileTimeBinding`.
auto AddCompileTimeBinding() -> SemIR::CompileTimeBindIndex {
auto index = scope_stack_.back().next_compile_time_bind_index;
++scope_stack_.back().next_compile_time_bind_index.index;
return index;
}
// Pushes a compile-time binding into the current scope.
auto PushCompileTimeBinding(SemIR::InstId bind_id) -> void {
compile_time_binding_stack_.AppendToTop(bind_id);
}
// Temporarily removes the top of the stack and its lexical lookup results.
auto Suspend() -> SuspendedScope;
// Restores a suspended scope stack entry.
auto Restore(SuspendedScope&& scope) -> void;
// Runs verification that the processing cleanly finished.
auto VerifyOnFinish() const -> void;
// Returns whether this is a scope in which cleanups are tracked.
auto IsCleanupScope() const -> bool {
return Peek().cleanup_scope_kind != CleanupScopeKind::None;
}
// Registers a cleanup for `inst_id` within the current cleanup scope.
auto PushCleanupFor(SemIR::InstId inst_id) -> void {
CARBON_CHECK(IsCleanupScope());
destroy_id_stack_.push_back(inst_id);
}
// Returns all values on `destroy_id_stack_` added since `depth`.
auto GetCleanupsSince(CleanupScopeDepth depth) const
-> llvm::ArrayRef<SemIR::InstId> {
return llvm::ArrayRef(destroy_id_stack_).slice(depth.index);
}
// Add all cleanups created so far in this scope to the ambient state of the
// scope. This causes them to be deferred until the scope is exited.
auto DeferCleanups() -> void {
CARBON_CHECK(IsCleanupScope() ||
static_cast<size_t>(Peek().cleanup_scope_depth.index) ==
destroy_id_stack_.size());
scope_stack_.back().cleanup_scope_depth =
CleanupScopeDepth(destroy_id_stack_.size());
}
// Discards cleanups after the given depth, which must be within the current
// scope.
auto DiscardCleanupsSince(CleanupScopeDepth depth) -> void {
auto enclosing = enclosing_cleanup_scope_depth();
CARBON_CHECK(depth >= enclosing);
destroy_id_stack_.truncate(depth.index);
if (scope_stack_.back().cleanup_scope_depth.index > depth.index) {
// We have discarded ambient cleanups. Reduce the ambient cleanup depth to
// match. This happens when exiting the scope.
scope_stack_.back().cleanup_scope_depth = depth;
}
}
// Returns the current depth of the cleanup stack.
auto cleanup_scope_depth() const -> CleanupScopeDepth {
return CleanupScopeDepth(destroy_id_stack_.size());
}
// Returns the ambient depth of the cleanup stack in the current scope.
auto ambient_cleanup_scope_depth() const -> CleanupScopeDepth {
return Peek().cleanup_scope_depth;
}
// Returns the depth of the cleanup stack enclosing the current scope.
auto enclosing_cleanup_scope_depth() const -> CleanupScopeDepth {
return scope_stack_.size() < 2 ? CleanupScopeDepth(0)
: Peek(1).cleanup_scope_depth;
}
// Returns the depth of the cleanup stack enclosing this function scope.
auto function_cleanup_scope_depth() const -> CleanupScopeDepth {
return return_scope_stack_.back().cleanup_scope_depth;
}
auto break_continue_stack() -> llvm::SmallVector<BreakContinueScope>& {
return break_continue_stack_;
}
auto compile_time_binding_stack() -> ArrayStack<SemIR::InstId>& {
return compile_time_binding_stack_;
}
auto full_pattern_stack() -> FullPatternStack& { return full_pattern_stack_; }
private:
auto sem_ir() const -> const SemIR::File&;
auto lexical_lookup() -> LexicalLookup& { return lexical_lookup_; }
// An entry in scope_stack_.
struct ScopeStackEntry : public MoveOnly<ScopeStackEntry> {
auto is_lexical_scope() const -> bool { return !scope_id.has_value(); }
// The sequential index of this scope entry within the file.
ScopeIndex index;
// The instruction associated with this entry, if any. This can be one of:
//
// - A `ClassDecl`, for a class definition scope.
// - A `FunctionDecl`, for the outermost scope in a function
// definition.
// - Invalid, for any other scope.
SemIR::InstId scope_inst_id;
// The name scope associated with this entry, if any.
SemIR::NameScopeId scope_id;
// The specific associated with this entry, if any.
SemIR::SpecificId specific_id;
// The next compile-time binding index to allocate in this scope.
SemIR::CompileTimeBindIndex next_compile_time_bind_index;
// Whether lexical_lookup_ has load errors from this scope or an ancestor
// scope.
bool lexical_lookup_has_load_error;
// Whether a `returned var` was introduced in this scope, and needs to be
// unregistered when the scope ends.
bool has_returned_var = false;
// The kind of cleanup scope that is associated with this scope.
CleanupScopeKind cleanup_scope_kind = CleanupScopeKind::None;
// The ambient cleanup scope depth in this scope. This is the depth that we
// will return to at the end of a statement in this scope.
CleanupScopeDepth cleanup_scope_depth;
// Whether there are any ids in the `names` set.
int num_names = 0;
// Names which are registered with lexical_lookup_, and will need to be
// unregistered when the scope ends.
Set<SemIR::NameId, 16> names = {};
};
// A scope in which `return` can be used.
struct ReturnScope {
// The `FunctionDecl`.
SemIR::InstId decl_id;
// The value corresponding to the current `returned var`, if any. Will be
// set and unset as `returned var`s are declared and go out of scope.
SemIR::InstId returned_var = SemIR::InstId::None;
// The cleanup stack depth when entering the function body.
CleanupScopeDepth cleanup_scope_depth;
// When a nested scope interrupts a return scope, this is the index of the
// outermost interrupting scope (the one closest to the function scope).
// This can then be used to determine whether we're actually inside the most
// recent `ReturnScope`, or inside a different entity scope.
//
// This won't be set for functions directly inside functions, because they
// will have their own `ReturnScope`.
// For example, when a `class` is inside a `fn`, it interrupts the function
// body by setting this on `PushEntity`; `Pop` will set it back to `None`.
ScopeIndex nested_scope_index = ScopeIndex::None;
};
// Pushes a scope onto scope_stack_. NameScopeId::None is used for new scopes.
// lexical_lookup_has_load_error is used to limit diagnostics when a given
// namespace may contain a mix of both successful and failed name imports.
auto Push(SemIR::InstId scope_inst_id, SemIR::NameScopeId scope_id,
SemIR::SpecificId specific_id, CleanupScopeKind cleanup_scope_kind,
bool lexical_lookup_has_load_error) -> void;
auto Peek(int drop = 0) const -> const ScopeStackEntry& {
CARBON_DCHECK(drop < static_cast<int>(scope_stack_.size()));
return scope_stack_[scope_stack_.size() - 1 - drop];
}
// Returns whether lexical lookup currently has any load errors.
auto LexicalLookupHasLoadError() const -> bool {
return !scope_stack_.empty() &&
scope_stack_.back().lexical_lookup_has_load_error;
}
// If inside a return scope, marks a nested scope (see `nested_scope_index`).
// Called after pushing the new scope.
auto MarkNestingIfInReturnScope() -> void {
if (!return_scope_stack_.empty() &&
!return_scope_stack_.back().nested_scope_index.has_value()) {
return_scope_stack_.back().nested_scope_index = scope_stack_.back().index;
}
}
// Marks the name `name_id` as used at the given location.
auto MarkUsed(SemIR::NameId name_id, SemIR::LocId loc_id, bool is_reachable)
-> void;
// Checks that the provided scope's `next_compile_time_bind_index` matches the
// full size of the current `compile_time_binding_stack_`. The values should
// always match, and this is used to validate the correspondence during
// significant changes.
auto VerifyNextCompileTimeBindIndex(llvm::StringLiteral label,
const ScopeStackEntry& scope) -> void;
// Context, used only for checks and emitting diagnostics.
Context* context_;
// A stack of scopes from which we can `return`.
llvm::SmallVector<ReturnScope> return_scope_stack_;
// A stack of `break` and `continue` targets.
llvm::SmallVector<BreakContinueScope> break_continue_stack_;
// A stack for scope context.
llvm::SmallVector<ScopeStackEntry> scope_stack_;
// A stack of instances to destroy. This only has entries inside of function
// bodies, where destruction on scope exit is required.
llvm::SmallVector<SemIR::InstId> destroy_id_stack_;
// Information about non-lexical scopes. This is a subset of the entries and
// the information in scope_stack_.
llvm::SmallVector<NonLexicalScope> non_lexical_scope_stack_;
// A stack of the current compile time bindings.
ArrayStack<SemIR::InstId> compile_time_binding_stack_;
// The index of the next scope that will be pushed onto scope_stack_. The
// first is always the package scope.
ScopeIndex next_scope_index_ = ScopeIndex::Package;
// Tracks lexical lookup results.
LexicalLookup lexical_lookup_;
// Stack of full-patterns currently being checked.
FullPatternStack full_pattern_stack_;
};
struct ScopeStack::SuspendedScope : public MoveOnly<SuspendedScope> {
// An item that was suspended within this scope. This represents either a
// lexical lookup entry in this scope, or a compile time binding entry in this
// scope.
//
// TODO: For compile-time bindings, the common case is that they will both
// have a suspended lexical lookup entry and a suspended compile time binding
// entry. We should be able to store that as a single ScopeItem rather than
// two.
struct ScopeItem {
static constexpr uint32_t IndexForCompileTimeBinding = -1;
// The scope index for a LexicalLookup::SuspendedResult, or
// CompileTimeBindingIndex for a suspended compile time binding.
uint32_t index;
// The instruction within the scope.
SemIR::InstId inst_id;
// Whether the name was declared in a reachable position.
bool is_decl_reachable;
// The location of the first use of the name, if any.
SemIR::LocId use_loc_id;
};
// The suspended scope stack entry.
ScopeStackEntry entry;
// The list of items that were within this scope when it was suspended. The
// inline size is an attempt to keep the size of a `SuspendedFunction`
// reasonable while avoiding heap allocations most of the time.
llvm::SmallVector<ScopeItem, 8> suspended_items;
};
} // namespace Carbon::Check
#endif // CARBON_TOOLCHAIN_CHECK_SCOPE_STACK_H_