From fdfb1fb5efdebb7def0ea9d41eace31082f8159b Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Mon, 5 Feb 2024 16:59:52 -0800 Subject: [PATCH] Factor the scope stack and lexical lookups out of Check::Context. (#3688) --- toolchain/check/BUILD | 22 ++- toolchain/check/check.cpp | 10 +- toolchain/check/context.cpp | 127 ++------------ toolchain/check/context.h | 157 ++--------------- toolchain/check/decl_name_stack.cpp | 15 +- toolchain/check/handle_binding_pattern.cpp | 3 +- toolchain/check/handle_class.cpp | 4 +- toolchain/check/handle_codeblock.cpp | 4 +- toolchain/check/handle_function.cpp | 4 +- toolchain/check/handle_impl.cpp | 10 +- toolchain/check/handle_interface.cpp | 4 +- toolchain/check/handle_let.cpp | 4 +- toolchain/check/handle_struct.cpp | 6 +- toolchain/check/handle_variable.cpp | 2 +- toolchain/check/return.cpp | 2 +- toolchain/check/scope_stack.cpp | 145 ++++++++++++++++ toolchain/check/scope_stack.h | 186 +++++++++++++++++++++ 17 files changed, 416 insertions(+), 289 deletions(-) create mode 100644 toolchain/check/scope_stack.cpp create mode 100644 toolchain/check/scope_stack.h diff --git a/toolchain/check/BUILD b/toolchain/check/BUILD index 5a8cc24cdf25..907f4668ef3a 100644 --- a/toolchain/check/BUILD +++ b/toolchain/check/BUILD @@ -28,6 +28,25 @@ cc_library( ], ) +cc_library( + name = "scope_stack", + srcs = ["scope_stack.cpp"], + hdrs = [ + "lexical_lookup.h", + "scope_index.h", + "scope_stack.h", + ], + deps = [ + "//common:check", + "//common:ostream", + "//common:vlog", + "//toolchain/base:index_base", + "//toolchain/sem_ir:file", + "//toolchain/sem_ir:ids", + "@llvm-project//llvm:Support", + ], +) + cc_library( name = "context", srcs = [ @@ -46,17 +65,16 @@ cc_library( "decl_state.h", "eval.h", "inst_block_stack.h", - "lexical_lookup.h", "modifiers.h", "pending_block.h", "return.h", - "scope_index.h", ], deps = [ ":node_stack", "//common:check", "//common:vlog", "//toolchain/base:index_base", + "//toolchain/check:scope_stack", "//toolchain/lex:tokenized_buffer", "//toolchain/parse:node_kind", "//toolchain/parse:tree", diff --git a/toolchain/check/check.cpp b/toolchain/check/check.cpp index 5052b9795c8d..4af321705e0b 100644 --- a/toolchain/check/check.cpp +++ b/toolchain/check/check.cpp @@ -175,13 +175,13 @@ static auto InitPackageScopeAndImports(Context& context, UnitInfo& unit_info) if (error_in_import) { context.name_scopes().Get(SemIR::NameScopeId::Package).has_error = true; } - context.PushScope(package_inst_id, SemIR::NameScopeId::Package, - error_in_import); + context.scope_stack().Push(package_inst_id, SemIR::NameScopeId::Package, + error_in_import); } else { // Push the scope; there are no names to add. - context.PushScope(package_inst_id, SemIR::NameScopeId::Package); + context.scope_stack().Push(package_inst_id, SemIR::NameScopeId::Package); } - CARBON_CHECK(context.current_scope_index() == ScopeIndex::Package); + CARBON_CHECK(context.scope_stack().PeekIndex() == ScopeIndex::Package); for (auto& [package_id, package_imports] : unit_info.package_imports_map) { if (!package_id.is_valid()) { @@ -255,7 +255,7 @@ static auto CheckParseTree( // Pop information for the file-level scope. sem_ir.set_top_inst_block_id(context.inst_block_stack().Pop()); - context.PopScope(); + context.scope_stack().Pop(); context.FinalizeExports(); context.VerifyOnFinish(); diff --git a/toolchain/check/context.cpp b/toolchain/check/context.cpp index 2430a9c5f524..4b61a4e721bc 100644 --- a/toolchain/check/context.cpp +++ b/toolchain/check/context.cpp @@ -38,7 +38,7 @@ Context::Context(const Lex::TokenizedBuffer& tokens, DiagnosticEmitter& emitter, params_or_args_stack_("params_or_args_stack_", sem_ir, vlog_stream), args_type_info_stack_("args_type_info_stack_", sem_ir, vlog_stream), decl_name_stack_(this), - lexical_lookup_(sem_ir_->identifiers()) { + scope_stack_(sem_ir_->identifiers()) { // Map the builtin `` and `type` type constants to their corresponding // special `TypeId` values. type_ids_for_type_constants_.insert( @@ -61,7 +61,7 @@ auto Context::VerifyOnFinish() -> void { // various pieces of context go out of scope. At this point, nothing should // remain. // node_stack_ will still contain top-level entities. - CARBON_CHECK(scope_stack_.empty()) << scope_stack_.size(); + scope_stack_.VerifyOnFinish(); CARBON_CHECK(inst_block_stack_.empty()) << inst_block_stack_.size(); CARBON_CHECK(params_or_args_stack_.empty()) << params_or_args_stack_.size(); } @@ -206,18 +206,9 @@ auto Context::AddPackageImports(Parse::NodeId import_node, auto Context::AddNameToLookup(SemIR::NameId name_id, SemIR::InstId target_id) -> void { - if (current_scope().names.insert(name_id).second) { - // TODO: Reject if we previously performed a failed lookup for this name in - // this scope or a scope nested within it. - auto& lexical_results = lexical_lookup_.Get(name_id); - CARBON_CHECK(lexical_results.empty() || - lexical_results.back().scope_index < current_scope_index()) - << "Failed to clean up after scope nested within the current scope"; - lexical_results.push_back( - {.inst_id = target_id, .scope_index = current_scope_index()}); - } else { - DiagnoseDuplicateName(target_id, - lexical_lookup_.Get(name_id).back().inst_id); + if (auto existing = scope_stack().LookupOrAddName(name_id, target_id); + existing.is_valid()) { + DiagnoseDuplicateName(target_id, existing); } } @@ -287,15 +278,11 @@ auto Context::LookupNameInDecl(Parse::NodeId /*parse_node*/, // In this case, we're not in the correct scope to define a member of // class A, so we should reject, and we achieve this by not finding the // name A from the outer scope. - auto& lexical_results = lexical_lookup_.Get(name_id); - if (!lexical_results.empty()) { - auto result = lexical_results.back(); - if (result.scope_index == current_scope_index()) { - ResolveIfImportRefUnused(result.inst_id); - return result.inst_id; - } + auto result = scope_stack().LookupInCurrentScope(name_id); + if (result.is_valid()) { + ResolveIfImportRefUnused(result); } - return SemIR::InstId::Invalid; + return result; } else { // We do not look into `extend`ed scopes here. A qualified name in a // declaration must specify the exact scope in which the name was originally @@ -316,20 +303,11 @@ auto Context::LookupUnqualifiedName(Parse::NodeId parse_node, // Find the results from enclosing lexical scopes. These will be combined with // results from non-lexical scopes such as namespaces and classes. - llvm::ArrayRef lexical_results = - lexical_lookup_.Get(name_id); + auto [lexical_result, non_lexical_scopes] = + scope_stack().LookupInEnclosingScopes(name_id); // Walk the non-lexical scopes and perform lookups into each of them. - for (auto [index, name_scope_id] : llvm::reverse(non_lexical_scope_stack_)) { - // If the innermost lexical result is within this non-lexical scope, then - // it shadows all further non-lexical results and we're done. - if (!lexical_results.empty() && - lexical_results.back().scope_index > index) { - auto inst_id = lexical_results.back().inst_id; - ResolveIfImportRefUnused(inst_id); - return inst_id; - } - + for (auto [index, name_scope_id] : llvm::reverse(non_lexical_scopes)) { if (auto non_lexical_result = LookupQualifiedName(parse_node, name_id, name_scope_id, /*required=*/false); @@ -338,16 +316,13 @@ auto Context::LookupUnqualifiedName(Parse::NodeId parse_node, } } - if (!lexical_results.empty()) { - auto inst_id = lexical_results.back().inst_id; - ResolveIfImportRefUnused(inst_id); - return inst_id; + if (lexical_result.is_valid()) { + ResolveIfImportRefUnused(lexical_result); + return lexical_result; } // We didn't find anything at all. - if (!lexical_lookup_has_load_error_) { - DiagnoseNameNotFound(parse_node, name_id); - } + DiagnoseNameNotFound(parse_node, name_id); return SemIR::InstId::BuiltinError; } @@ -410,76 +385,6 @@ auto Context::LookupQualifiedName(Parse::NodeId parse_node, return result_id; } -auto Context::PushScope(SemIR::InstId scope_inst_id, - SemIR::NameScopeId scope_id, - bool lexical_lookup_has_load_error) -> void { - scope_stack_.push_back( - {.index = next_scope_index_, - .scope_inst_id = scope_inst_id, - .scope_id = scope_id, - .prev_lexical_lookup_has_load_error = lexical_lookup_has_load_error_}); - if (scope_id.is_valid()) { - non_lexical_scope_stack_.push_back({next_scope_index_, scope_id}); - } - - lexical_lookup_has_load_error_ |= lexical_lookup_has_load_error; - - // TODO: Handle this case more gracefully. - CARBON_CHECK(next_scope_index_.index != std::numeric_limits::max()) - << "Ran out of scopes"; - ++next_scope_index_.index; -} - -auto Context::PopScope() -> void { - auto scope = scope_stack_.pop_back_val(); - - lexical_lookup_has_load_error_ = scope.prev_lexical_lookup_has_load_error; - - for (const auto& str_id : scope.names) { - auto& lexical_results = lexical_lookup_.Get(str_id); - CARBON_CHECK(lexical_results.back().scope_index == scope.index) - << "Inconsistent scope index for name " << names().GetFormatted(str_id); - lexical_results.pop_back(); - } - - if (scope.scope_id.is_valid()) { - CARBON_CHECK(non_lexical_scope_stack_.back().first == scope.index); - non_lexical_scope_stack_.pop_back(); - } - - if (scope.has_returned_var) { - CARBON_CHECK(!return_scope_stack_.empty()); - CARBON_CHECK(return_scope_stack_.back().returned_var.is_valid()); - return_scope_stack_.back().returned_var = SemIR::InstId::Invalid; - } -} - -auto Context::PopToScope(ScopeIndex index) -> void { - while (current_scope_index() > index) { - PopScope(); - } - CARBON_CHECK(current_scope_index() == index) - << "Scope index " << index << " does not enclose the current scope " - << current_scope_index(); -} - -auto Context::SetReturnedVarOrGetExisting(SemIR::InstId inst_id) - -> SemIR::InstId { - CARBON_CHECK(!return_scope_stack_.empty()) << "`returned var` in no function"; - auto& returned_var = return_scope_stack_.back().returned_var; - if (returned_var.is_valid()) { - return returned_var; - } - - returned_var = inst_id; - CARBON_CHECK(!current_scope().has_returned_var) - << "Scope has returned var but none is set"; - if (inst_id.is_valid()) { - current_scope().has_returned_var = true; - } - return SemIR::InstId::Invalid; -} - template static auto AddDominatedBlockAndBranchImpl(Context& context, Parse::NodeId parse_node, diff --git a/toolchain/check/context.h b/toolchain/check/context.h index 47d6589345f8..418ce60169ff 100644 --- a/toolchain/check/context.h +++ b/toolchain/check/context.h @@ -6,14 +6,13 @@ #define CARBON_TOOLCHAIN_CHECK_CONTEXT_H_ #include "llvm/ADT/DenseMap.h" -#include "llvm/ADT/DenseSet.h" #include "llvm/ADT/FoldingSet.h" #include "llvm/ADT/SmallVector.h" #include "toolchain/check/decl_name_stack.h" #include "toolchain/check/decl_state.h" #include "toolchain/check/inst_block_stack.h" -#include "toolchain/check/lexical_lookup.h" #include "toolchain/check/node_stack.h" +#include "toolchain/check/scope_stack.h" #include "toolchain/parse/node_ids.h" #include "toolchain/parse/tree.h" #include "toolchain/parse/tree_node_location_translator.h" @@ -51,23 +50,6 @@ class Context { using DiagnosticEmitter = Carbon::DiagnosticEmitter; using DiagnosticBuilder = DiagnosticEmitter::DiagnosticBuilder; - // A scope in which `break` and `continue` can be used. - struct BreakContinueScope { - SemIR::InstBlockId break_target; - SemIR::InstBlockId continue_target; - }; - - // A scope in which `return` can be used. - struct ReturnScope { - // The declaration from which we can return. Inside a function, this will - // be a `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::Invalid; - }; - // Stores references for work. explicit Context(const Lex::TokenizedBuffer& tokens, DiagnosticEmitter& emitter, const Parse::Tree& parse_tree, @@ -168,68 +150,13 @@ class Context { auto NoteIncompleteClass(SemIR::ClassId class_id, DiagnosticBuilder& builder) -> void; - // Pushes a scope onto scope_stack_. NameScopeId::Invalid 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 PushScope(SemIR::InstId scope_inst_id = SemIR::InstId::Invalid, - SemIR::NameScopeId scope_id = SemIR::NameScopeId::Invalid, - bool lexical_lookup_has_load_error = false) -> void; - - // Pops the top scope from scope_stack_, cleaning up names from - // lexical_lookup_. - auto PopScope() -> void; - - // Pops the top scope from scope_stack_ if it contains no names. - auto PopScopeIfEmpty() -> void { - if (scope_stack_.back().names.empty()) { - PopScope(); - } - } - - // Pops scopes until we return to the specified scope index. - auto PopToScope(ScopeIndex index) -> void; - - // Returns the scope index associated with the current scope. - auto current_scope_index() const -> ScopeIndex { - return current_scope().index; - } - - // Returns the name scope associated with the current lexical scope, if any. - auto current_scope_id() const -> SemIR::NameScopeId { - return current_scope().scope_id; - } - - // Returns the instruction associated with the current scope, or Invalid if - // there is no such instruction, such as for a block scope. - auto current_scope_inst_id() const -> SemIR::InstId { - return current_scope().scope_inst_id; - } - - auto GetCurrentScopeParseNode() const -> Parse::NodeId { - auto inst_id = current_scope_inst_id(); - if (!inst_id.is_valid()) { - return Parse::NodeId::Invalid; - } - return sem_ir_->insts().GetParseNode(inst_id); - } - // Returns the current scope, if it is of the specified kind. Otherwise, // returns nullopt. template auto GetCurrentScopeAs() -> std::optional { - auto inst_id = current_scope_inst_id(); - if (!inst_id.is_valid()) { - return std::nullopt; - } - return insts().TryGetAs(inst_id); + return scope_stack().GetCurrentScopeAs(sem_ir()); } - // If there is no `returned var` in scope, sets the given instruction to be - // the current `returned var` and returns an invalid instruction ID. If there - // is already a `returned var`, returns it instead. - auto SetReturnedVarOrGetExisting(SemIR::InstId inst_id) -> SemIR::InstId; - // Adds a `Branch` instruction branching to a new instruction block, and // returns the ID of the new block. All paths to the branch target must go // through the current block, though not necessarily through this branch. @@ -391,19 +318,20 @@ class Context { return args_type_info_stack_; } - auto return_scope_stack() -> llvm::SmallVector& { - return return_scope_stack_; - } - - auto break_continue_stack() -> llvm::SmallVector& { - return break_continue_stack_; - } - auto decl_name_stack() -> DeclNameStack& { return decl_name_stack_; } auto decl_state_stack() -> DeclStateStack& { return decl_state_stack_; } - auto lexical_lookup() -> LexicalLookup& { return lexical_lookup_; } + auto scope_stack() -> ScopeStack& { return scope_stack_; } + + auto return_scope_stack() -> llvm::SmallVector& { + return scope_stack().return_scope_stack(); + } + + auto break_continue_stack() + -> llvm::SmallVector& { + return scope_stack().break_continue_stack(); + } // Directly expose SemIR::File data accessors for brevity in calls. @@ -461,45 +389,10 @@ class Context { SemIR::TypeId type_id_; }; - // An entry in scope_stack_. - struct ScopeStackEntry { - // 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 previous state of lexical_lookup_has_load_error_, restored on pop. - bool prev_lexical_lookup_has_load_error; - - // Names which are registered with lexical_lookup_, and will need to be - // unregistered when the scope ends. - llvm::DenseSet names; - - // Whether a `returned var` was introduced in this scope, and needs to be - // unregistered when the scope ends. - bool has_returned_var = false; - - // TODO: This likely needs to track things which need to be destructed. - }; - // If the passed in instruction ID is a ImportRefUnused, resolves it for use. // Called when name lookup intends to return an inst_id. auto ResolveIfImportRefUnused(SemIR::InstId inst_id) -> void; - auto current_scope() -> ScopeStackEntry& { return scope_stack_.back(); } - auto current_scope() const -> const ScopeStackEntry& { - return scope_stack_.back(); - } - // Tokens for getting data on literals. const Lex::TokenizedBuffer* tokens_; @@ -533,36 +426,14 @@ class Context { // for a type separate from the literal arguments. InstBlockStack args_type_info_stack_; - // A stack of scopes from which we can `return`. - llvm::SmallVector return_scope_stack_; - - // A stack of `break` and `continue` targets. - llvm::SmallVector break_continue_stack_; - - // A stack for scope context. - llvm::SmallVector scope_stack_; - - // Information about non-lexical scopes. This is a subset of the entries and - // the information in scope_stack_. - llvm::SmallVector> - non_lexical_scope_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; - // The stack used for qualified declaration name construction. DeclNameStack decl_name_stack_; // The stack of declarations that could have modifiers. DeclStateStack decl_state_stack_; - // Tracks lexical lookup results. - LexicalLookup lexical_lookup_; - - // Whether lexical_lookup_ has load errors, updated whenever scope_stack_ is - // pushed or popped. - bool lexical_lookup_has_load_error_ = false; + // The stack of scopes we are currently within. + ScopeStack scope_stack_; // Cache of reverse mapping from type constants to types. // diff --git a/toolchain/check/decl_name_stack.cpp b/toolchain/check/decl_name_stack.cpp index 67d4037a55e3..0b6fa8b25b70 100644 --- a/toolchain/check/decl_name_stack.cpp +++ b/toolchain/check/decl_name_stack.cpp @@ -10,8 +10,9 @@ namespace Carbon::Check { auto DeclNameStack::MakeEmptyNameContext() -> NameContext { - return NameContext{.enclosing_scope = context_->current_scope_index(), - .target_scope_id = context_->current_scope_id()}; + return NameContext{ + .enclosing_scope = context_->scope_stack().PeekIndex(), + .target_scope_id = context_->scope_stack().PeekNameScopeId()}; } auto DeclNameStack::MakeUnqualifiedName(Parse::NodeId parse_node, @@ -25,7 +26,7 @@ auto DeclNameStack::PushScopeAndStartName() -> void { decl_name_stack_.push_back(MakeEmptyNameContext()); // Create a scope for any parameters introduced in this name. - context_->PushScope(); + context_->scope_stack().Push(); } auto DeclNameStack::FinishName() -> NameContext { @@ -49,7 +50,7 @@ auto DeclNameStack::FinishName() -> NameContext { auto DeclNameStack::PopScope() -> void { CARBON_CHECK(decl_name_stack_.back().state == NameContext::State::Finished) << "Missing call to FinishName before PopScope"; - context_->PopToScope(decl_name_stack_.back().enclosing_scope); + context_->scope_stack().PopTo(decl_name_stack_.back().enclosing_scope); decl_name_stack_.pop_back(); } @@ -154,12 +155,12 @@ static auto PushNameQualifierScope(Context& context, bool has_error = false) -> void { // If the qualifier has no parameters, we don't need to keep around a // parameter scope. - context.PopScopeIfEmpty(); + context.scope_stack().PopIfEmpty(); - context.PushScope(scope_inst_id, scope_id, has_error); + context.scope_stack().Push(scope_inst_id, scope_id, has_error); // Enter a parameter scope in case the qualified name itself has parameters. - context.PushScope(); + context.scope_stack().Push(); } auto DeclNameStack::UpdateScopeIfNeeded(NameContext& name_context) -> void { diff --git a/toolchain/check/handle_binding_pattern.cpp b/toolchain/check/handle_binding_pattern.cpp index deafb822c7ce..85c6fe0f3995 100644 --- a/toolchain/check/handle_binding_pattern.cpp +++ b/toolchain/check/handle_binding_pattern.cpp @@ -26,7 +26,8 @@ auto HandleAnyBindingPattern(Context& context, Parse::NodeId parse_node, // TODO: Eventually the name will need to support associations with other // scopes, but right now we don't support qualified names here. auto bind_name_id = context.bind_names().Add( - {.name_id = name_id, .enclosing_scope_id = context.current_scope_id()}); + {.name_id = name_id, + .enclosing_scope_id = context.scope_stack().PeekNameScopeId()}); if (is_generic) { // TODO: Create a `BindTemplateName` instead inside a `template` pattern. return {name_node, diff --git a/toolchain/check/handle_class.cpp b/toolchain/check/handle_class.cpp index 6649c5d1223f..ddc6270cb6bf 100644 --- a/toolchain/check/handle_class.cpp +++ b/toolchain/check/handle_class.cpp @@ -144,7 +144,7 @@ auto HandleClassDefinitionStart(Context& context, } // Enter the class scope. - context.PushScope(class_decl_id, class_info.scope_id); + context.scope_stack().Push(class_decl_id, class_info.scope_id); // Introduce `Self`. context.AddNameToLookup(SemIR::NameId::SelfType, @@ -329,7 +329,7 @@ auto HandleClassDefinition(Context& context, auto class_id = context.node_stack().Pop(); context.inst_block_stack().Pop(); - context.PopScope(); + context.scope_stack().Pop(); context.decl_name_stack().PopScope(); // The class type is now fully defined. diff --git a/toolchain/check/handle_codeblock.cpp b/toolchain/check/handle_codeblock.cpp index d7073998531e..81cdc374d118 100644 --- a/toolchain/check/handle_codeblock.cpp +++ b/toolchain/check/handle_codeblock.cpp @@ -9,13 +9,13 @@ namespace Carbon::Check { auto HandleCodeBlockStart(Context& context, Parse::CodeBlockStartId parse_node) -> bool { context.node_stack().Push(parse_node); - context.PushScope(); + context.scope_stack().Push(); return true; } auto HandleCodeBlock(Context& context, Parse::CodeBlockId /*parse_node*/) -> bool { - context.PopScope(); + context.scope_stack().Pop(); context.node_stack() .PopAndDiscardSoloParseNode(); return true; diff --git a/toolchain/check/handle_function.cpp b/toolchain/check/handle_function.cpp index ef61cd009b97..ac01bbc82521 100644 --- a/toolchain/check/handle_function.cpp +++ b/toolchain/check/handle_function.cpp @@ -220,7 +220,7 @@ auto HandleFunctionDefinitionStart(Context& context, // Create the function scope and the entry block. context.return_scope_stack().push_back({.decl_id = decl_id}); context.inst_block_stack().Push(); - context.PushScope(decl_id); + context.scope_stack().Push(decl_id); context.AddCurrentCodeBlockToFunction(); // Bring the implicit and explicit parameters into scope. @@ -270,7 +270,7 @@ auto HandleFunctionDefinition(Context& context, } } - context.PopScope(); + context.scope_stack().Pop(); context.inst_block_stack().Pop(); context.return_scope_stack().pop_back(); context.decl_name_stack().PopScope(); diff --git a/toolchain/check/handle_impl.cpp b/toolchain/check/handle_impl.cpp index 754199e58fab..26041f2d40d9 100644 --- a/toolchain/check/handle_impl.cpp +++ b/toolchain/check/handle_impl.cpp @@ -20,7 +20,7 @@ auto HandleImplIntroducer(Context& context, Parse::ImplIntroducerId parse_node) // Create a scope for implicit parameters. We may not use it, but it's simpler // to create it unconditionally than to track whether it exists. - context.PushScope(); + context.scope_stack().Push(); return true; } @@ -66,7 +66,7 @@ static auto BuildImplDecl(Context& context, Parse::AnyImplDeclId /*parse_node*/) auto HandleImplDecl(Context& context, Parse::ImplDeclId parse_node) -> bool { BuildImplDecl(context, parse_node); - context.PopScope(); + context.scope_stack().Pop(); return true; } @@ -77,14 +77,14 @@ auto HandleImplDefinitionStart(Context& context, auto enclosing_scope_id = SemIR::NameScopeId::Invalid; auto scope_id = context.name_scopes().Add( impl_decl_id, SemIR::NameId::Invalid, enclosing_scope_id); - context.PushScope(impl_decl_id, scope_id); + context.scope_stack().Push(impl_decl_id, scope_id); return true; } auto HandleImplDefinition(Context& context, Parse::ImplDefinitionId /*parse_node*/) -> bool { - context.PopScope(); - context.PopScope(); + context.scope_stack().Pop(); + context.scope_stack().Pop(); return true; } diff --git a/toolchain/check/handle_interface.cpp b/toolchain/check/handle_interface.cpp index 6843ebae0ea3..6bfc016a8bb4 100644 --- a/toolchain/check/handle_interface.cpp +++ b/toolchain/check/handle_interface.cpp @@ -125,7 +125,7 @@ auto HandleInterfaceDefinitionStart( } // Enter the interface scope. - context.PushScope(interface_decl_id, interface_info.scope_id); + context.scope_stack().Push(interface_decl_id, interface_info.scope_id); // TODO: Introduce `Self`. @@ -152,7 +152,7 @@ auto HandleInterfaceDefinition(Context& context, auto interface_id = context.node_stack().Pop(); context.inst_block_stack().Pop(); - context.PopScope(); + context.scope_stack().Pop(); context.decl_name_stack().PopScope(); // The interface type is now fully defined. diff --git a/toolchain/check/handle_let.cpp b/toolchain/check/handle_let.cpp index 5f0d84bf762e..c4acaf20102b 100644 --- a/toolchain/check/handle_let.cpp +++ b/toolchain/check/handle_let.cpp @@ -34,9 +34,9 @@ auto HandleLetDecl(Context& context, Parse::LetDeclId parse_node) -> bool { // TODO: For a qualified `let` declaration, this should use the target scope // of the name introduced in the declaration. See #2590. CheckAccessModifiersOnDecl(context, Lex::TokenKind::Let, - context.current_scope_id()); + context.scope_stack().PeekNameScopeId()); RequireDefaultFinalOnlyInInterfaces(context, Lex::TokenKind::Let, - context.current_scope_id()); + context.scope_stack().PeekNameScopeId()); LimitModifiersOnDecl( context, KeywordModifierSet::Access | KeywordModifierSet::Interface, Lex::TokenKind::Let); diff --git a/toolchain/check/handle_struct.cpp b/toolchain/check/handle_struct.cpp index ad12134367e0..7c6d42b61d7d 100644 --- a/toolchain/check/handle_struct.cpp +++ b/toolchain/check/handle_struct.cpp @@ -10,7 +10,7 @@ namespace Carbon::Check { auto HandleStructLiteralOrStructTypeLiteralStart( Context& context, Parse::StructLiteralOrStructTypeLiteralStartId parse_node) -> bool { - context.PushScope(); + context.scope_stack().Push(); context.node_stack().Push(parse_node); // At this point we aren't sure whether this will be a value or type literal, // so we push onto args irrespective. It just won't be used for a type @@ -94,7 +94,7 @@ auto HandleStructLiteral(Context& context, Parse::StructLiteralId parse_node) auto refs_id = context.ParamOrArgEnd( Parse::NodeKind::StructLiteralOrStructTypeLiteralStart); - context.PopScope(); + context.scope_stack().Pop(); context.node_stack() .PopAndDiscardSoloParseNode< Parse::NodeKind::StructLiteralOrStructTypeLiteralStart>(); @@ -117,7 +117,7 @@ auto HandleStructTypeLiteral(Context& context, auto refs_id = context.ParamOrArgEnd( Parse::NodeKind::StructLiteralOrStructTypeLiteralStart); - context.PopScope(); + context.scope_stack().Pop(); context.node_stack() .PopAndDiscardSoloParseNode< Parse::NodeKind::StructLiteralOrStructTypeLiteralStart>(); diff --git a/toolchain/check/handle_variable.cpp b/toolchain/check/handle_variable.cpp index 5d93c9934510..4977b1c55e54 100644 --- a/toolchain/check/handle_variable.cpp +++ b/toolchain/check/handle_variable.cpp @@ -85,7 +85,7 @@ auto HandleVariableDecl(Context& context, Parse::VariableDeclId parse_node) // TODO: For a qualified `var` declaration, this should use the target scope // of the name introduced in the declaration. See #2590. CheckAccessModifiersOnDecl(context, Lex::TokenKind::Var, - context.current_scope_id()); + context.scope_stack().PeekNameScopeId()); LimitModifiersOnDecl(context, KeywordModifierSet::Access, Lex::TokenKind::Var); auto modifiers = context.decl_state_stack().innermost().modifier_set; diff --git a/toolchain/check/return.cpp b/toolchain/check/return.cpp index 24d8ff1919de..cd49c47d8f76 100644 --- a/toolchain/check/return.cpp +++ b/toolchain/check/return.cpp @@ -91,7 +91,7 @@ auto CheckReturnedVar(Context& context, Parse::NodeId returned_node, } auto RegisterReturnedVar(Context& context, SemIR::InstId bind_id) -> void { - auto existing_id = context.SetReturnedVarOrGetExisting(bind_id); + auto existing_id = context.scope_stack().SetReturnedVarOrGetExisting(bind_id); if (existing_id.is_valid()) { CARBON_DIAGNOSTIC(ReturnedVarShadowed, Error, "Cannot declare a `returned var` in the scope of " diff --git a/toolchain/check/scope_stack.cpp b/toolchain/check/scope_stack.cpp new file mode 100644 index 000000000000..619a470f1831 --- /dev/null +++ b/toolchain/check/scope_stack.cpp @@ -0,0 +1,145 @@ +// 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 + +#include "toolchain/check/scope_stack.h" + +#include "common/check.h" +#include "toolchain/sem_ir/ids.h" + +namespace Carbon::Check { + +auto ScopeStack::VerifyOnFinish() -> void { + CARBON_CHECK(scope_stack_.empty()) << scope_stack_.size(); +} + +auto ScopeStack::Push(SemIR::InstId scope_inst_id, SemIR::NameScopeId scope_id, + bool lexical_lookup_has_load_error) -> void { + scope_stack_.push_back( + {.index = next_scope_index_, + .scope_inst_id = scope_inst_id, + .scope_id = scope_id, + .prev_lexical_lookup_has_load_error = lexical_lookup_has_load_error_}); + if (scope_id.is_valid()) { + non_lexical_scope_stack_.push_back({next_scope_index_, scope_id}); + } + + lexical_lookup_has_load_error_ |= lexical_lookup_has_load_error; + + // TODO: Handle this case more gracefully. + CARBON_CHECK(next_scope_index_.index != std::numeric_limits::max()) + << "Ran out of scopes"; + ++next_scope_index_.index; +} + +auto ScopeStack::Pop() -> void { + auto scope = scope_stack_.pop_back_val(); + + lexical_lookup_has_load_error_ = scope.prev_lexical_lookup_has_load_error; + + for (const auto& str_id : scope.names) { + auto& lexical_results = lexical_lookup_.Get(str_id); + CARBON_CHECK(lexical_results.back().scope_index == scope.index) + << "Inconsistent scope index for name " << str_id; + lexical_results.pop_back(); + } + + if (scope.scope_id.is_valid()) { + CARBON_CHECK(non_lexical_scope_stack_.back().scope_index == scope.index); + non_lexical_scope_stack_.pop_back(); + } + + if (scope.has_returned_var) { + CARBON_CHECK(!return_scope_stack_.empty()); + CARBON_CHECK(return_scope_stack_.back().returned_var.is_valid()); + return_scope_stack_.back().returned_var = SemIR::InstId::Invalid; + } +} + +auto ScopeStack::PopTo(ScopeIndex index) -> void { + while (PeekIndex() > index) { + Pop(); + } + CARBON_CHECK(PeekIndex() == index) + << "Scope index " << index << " does not enclose the current scope " + << PeekIndex(); +} + +auto ScopeStack::LookupInCurrentScope(SemIR::NameId name_id) -> SemIR::InstId { + auto& lexical_results = lexical_lookup_.Get(name_id); + if (lexical_results.empty()) { + return SemIR::InstId::Invalid; + } + + auto result = lexical_results.back(); + if (result.scope_index != PeekIndex()) { + return SemIR::InstId::Invalid; + } + + return result.inst_id; +} + +auto ScopeStack::LookupInEnclosingScopes(SemIR::NameId name_id) + -> std::pair> { + // Find the results from enclosing lexical scopes. These will be combined with + // results from non-lexical scopes such as namespaces and classes. + llvm::ArrayRef lexical_results = + lexical_lookup_.Get(name_id); + + // If we have no lexical results, check all non-lexical scopes. + if (lexical_results.empty()) { + return {lexical_lookup_has_load_error_ ? SemIR::InstId::BuiltinError + : SemIR::InstId::Invalid, + non_lexical_scope_stack_}; + } + + // Find the first non-lexical scope that is within the scope of the lexical + // lookup result. + auto* first_non_lexical_scope = std::lower_bound( + non_lexical_scope_stack_.begin(), non_lexical_scope_stack_.end(), + lexical_results.back().scope_index, + [](const NonLexicalScope& scope, ScopeIndex index) { + return scope.scope_index < index; + }); + return { + lexical_results.back().inst_id, + llvm::ArrayRef(first_non_lexical_scope, non_lexical_scope_stack_.end())}; +} + +auto ScopeStack::LookupOrAddName(SemIR::NameId name_id, SemIR::InstId target_id) + -> SemIR::InstId { + if (!scope_stack_.back().names.insert(name_id).second) { + auto existing = lexical_lookup_.Get(name_id).back().inst_id; + CARBON_CHECK(existing.is_valid()) + << "Name in scope but not in lexical lookups"; + return existing; + } + + // TODO: Reject if we previously performed a failed lookup for this name + // in this scope or a scope nested within it. + auto& lexical_results = lexical_lookup_.Get(name_id); + CARBON_CHECK(lexical_results.empty() || + lexical_results.back().scope_index < PeekIndex()) + << "Failed to clean up after scope nested within the current scope"; + lexical_results.push_back({.inst_id = target_id, .scope_index = PeekIndex()}); + return SemIR::InstId::Invalid; +} + +auto ScopeStack::SetReturnedVarOrGetExisting(SemIR::InstId inst_id) + -> SemIR::InstId { + CARBON_CHECK(!return_scope_stack_.empty()) << "`returned var` in no function"; + auto& returned_var = return_scope_stack_.back().returned_var; + if (returned_var.is_valid()) { + return returned_var; + } + + returned_var = inst_id; + CARBON_CHECK(!scope_stack_.back().has_returned_var) + << "Scope has returned var but none is set"; + if (inst_id.is_valid()) { + scope_stack_.back().has_returned_var = true; + } + return SemIR::InstId::Invalid; +} + +} // namespace Carbon::Check diff --git a/toolchain/check/scope_stack.h b/toolchain/check/scope_stack.h new file mode 100644 index 000000000000..f4a086ec6ef1 --- /dev/null +++ b/toolchain/check/scope_stack.h @@ -0,0 +1,186 @@ +// 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 "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.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 { + +// A stack of lexical and semantic scopes that we are currently performing +// checking within. +class ScopeStack { + public: + explicit ScopeStack(const StringStoreWrapper& identifiers) + : lexical_lookup_(identifiers) {} + + // A scope in which `break` and `continue` can be used. + struct BreakContinueScope { + SemIR::InstBlockId break_target; + SemIR::InstBlockId continue_target; + }; + + // A scope in which `return` can be used. + struct ReturnScope { + // The declaration from which we can return. Inside a function, this will + // be a `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::Invalid; + }; + + // 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; + }; + + // Pushes a scope onto scope_stack_. NameScopeId::Invalid 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::InstId::Invalid, + SemIR::NameScopeId scope_id = SemIR::NameScopeId::Invalid, + bool lexical_lookup_has_load_error = false) -> void; + + // Pops the top scope from scope_stack_, cleaning up names from + // lexical_lookup_. + auto Pop() -> void; + + // Pops the top scope from scope_stack_ if it contains no names. + auto PopIfEmpty() -> void { + if (scope_stack_.back().names.empty()) { + Pop(); + } + } + + // Pops scopes until we return to the specified scope index. + auto PopTo(ScopeIndex index) -> 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 Invalid if + // there is no such instruction, such as for a block scope. + auto PeekInstId() const -> SemIR::InstId { return Peek().scope_inst_id; } + + // Returns the current scope, if it is of the specified kind. Otherwise, + // returns nullopt. + template + auto GetCurrentScopeAs(const SemIR::File& sem_ir) -> std::optional { + auto inst_id = PeekInstId(); + if (!inst_id.is_valid()) { + return std::nullopt; + } + return sem_ir.insts().TryGetAs(inst_id); + } + + // If there is no `returned var` in scope, sets the given instruction to be + // the current `returned var` and returns an invalid instruction ID. If there + // is already a `returned var`, returns it instead. + auto SetReturnedVarOrGetExisting(SemIR::InstId inst_id) -> SemIR::InstId; + + // Looks up the name `name_id` in the current scope. Returns the existing + // lookup result, if any. + auto LookupInCurrentScope(SemIR::NameId name_id) -> SemIR::InstId; + + // Looks up the name `name_id` in the current scope and its enclosing 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. + auto LookupInEnclosingScopes(SemIR::NameId name_id) + -> std::pair>; + + // Looks up the name `name_id` in the current scope. Returns the existing + // instruction if any, and otherwise adds the name with the value `target_id` + // and returns Invalid. + auto LookupOrAddName(SemIR::NameId name_id, SemIR::InstId target_id) + -> SemIR::InstId; + + // Runs verification that the processing cleanly finished. + auto VerifyOnFinish() -> void; + + auto return_scope_stack() -> llvm::SmallVector& { + return return_scope_stack_; + } + + auto break_continue_stack() -> llvm::SmallVector& { + return break_continue_stack_; + } + + private: + // An entry in scope_stack_. + struct ScopeStackEntry { + // 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 previous state of lexical_lookup_has_load_error_, restored on pop. + bool prev_lexical_lookup_has_load_error; + + // Names which are registered with lexical_lookup_, and will need to be + // unregistered when the scope ends. + llvm::DenseSet names; + + // Whether a `returned var` was introduced in this scope, and needs to be + // unregistered when the scope ends. + bool has_returned_var = false; + + // TODO: This likely needs to track things which need to be destructed. + }; + + auto Peek() const -> const ScopeStackEntry& { return scope_stack_.back(); } + + // A stack of scopes from which we can `return`. + llvm::SmallVector return_scope_stack_; + + // A stack of `break` and `continue` targets. + llvm::SmallVector break_continue_stack_; + + // A stack for scope context. + llvm::SmallVector scope_stack_; + + // Information about non-lexical scopes. This is a subset of the entries and + // the information in scope_stack_. + llvm::SmallVector non_lexical_scope_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_; + + // Whether lexical_lookup_ has load errors, updated whenever scope_stack_ is + // pushed or popped. + bool lexical_lookup_has_load_error_ = false; +}; + +} // namespace Carbon::Check + +#endif // CARBON_TOOLCHAIN_CHECK_SCOPE_STACK_H_