Skip to content

Commit

Permalink
chapel-py: fix "IDs available in scope" (#24988)
Browse files Browse the repository at this point in the history
The `doLookupInScope` family of functions have special logic to
introduce the symbol-to-be-imported into the search results.
Specifically, use statements in the form `use IO` bring `IO` into scope
in addition the symbols from `IO`. Prior to this PR,
`getSymbolsAvailableInScope` did not handle this, assuming that
iteration over `scope->declared()` was sufficient. This PR addresses
that problem.

As a result, renaming modules now works properly with autocompletions in
CLS, and the language server doesn't need to rely on
`findUsedImportedIds`.


![modulerename](https://github.com/chapel-lang/chapel/assets/4361282/b43e253d-0822-4fa2-9d8a-ab2186fca969)

Reviewed by @jabraham17 and @mppf -- thanks!

## Testing

- [x] dyno tests
- [x] paratest
  • Loading branch information
DanilaFe authored May 6, 2024
2 parents 8de6282 + cdba373 commit f51e663
Show file tree
Hide file tree
Showing 4 changed files with 276 additions and 48 deletions.
111 changes: 73 additions & 38 deletions frontend/lib/resolution/scope-queries.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,30 @@ getKindForVisibilityClauseId(Context* context, ID visibilityClauseId) {
return VIS_USE;
}

// Creates a BorrowedIdsWithName for the symbol that defines a scope.
// Used for capturing 'IO' when a visibility statement is 'use IO'.
static optional<BorrowedIdsWithName>
borrowedIdsForScopeSymbol(Context* context,
IdAndFlags::Flags filterFlags,
const IdAndFlags::FlagSet& excludeFilter,
const Scope* scope) {
// Note: for the purposes of scope resolution, there shouldn't be a need
// to distinguish between public and default visibility, so the below
// ternary is sufficient.
auto visibility =
parsing::idIsPrivateDecl(context, scope->id()) ? Decl::PRIVATE : Decl::PUBLIC;
bool isField = false; // target must be module/enum, not field
bool isMethod = false; // target must be module/enum, not method
bool isParenfulFunction = false;
return BorrowedIdsWithName::createWithSingleId(scope->id(),
visibility,
isField,
isMethod,
isParenfulFunction,
filterFlags,
excludeFilter);
}

// config has settings for this part of the search
// filterFlags has the filter used when considering the module name itself
bool LookupHelper::doLookupInImportsAndUses(
Expand Down Expand Up @@ -739,19 +763,10 @@ bool LookupHelper::doLookupInImportsAndUses(

if (named && is.kind() == VisibilitySymbols::SYMBOL_ONLY) {
// Make sure the module / enum being renamed isn't private.
auto scopeAst = parsing::idToAst(context, is.scope()->id());
auto visibility = scopeAst->toDecl()->visibility();
bool isField = false; // target must be module/enum, not field
bool isMethod = false; // target must be module/enum, not method
bool isParenfulFunction = false;
auto foundIds =
BorrowedIdsWithName::createWithSingleId(is.scope()->id(),
visibility,
isField,
isMethod,
isParenfulFunction,
auto foundIds = borrowedIdsForScopeSymbol(context,
filterFlags,
excludeFilter);
excludeFilter,
is.scope());
if (foundIds) {
if (trace) {
ResultVisibilityTrace t;
Expand Down Expand Up @@ -1652,48 +1667,66 @@ getSymbolsAvailableInScopeQuery(Context* context,

std::map<UniqueString, BorrowedIdsWithName> toReturn;

auto allowedByVisibility = [inVisibilitySymbols](UniqueString name, UniqueString& renameTo) {
auto allowedByVisibility = [inVisibilitySymbols](UniqueString name,
UniqueString& renameTo,
bool isSymbolItself) {
renameTo = name;

if (!inVisibilitySymbols) return true;
if (!inVisibilitySymbols) return !isSymbolItself;
auto kind = inVisibilitySymbols->kind();

if (kind == VisibilitySymbols::ALL_CONTENTS) {
return true;
// ALL_CONTENTS brings in the contents, but not the symbol itself.
return !isSymbolItself;
}

if (kind == VisibilitySymbols::ONLY_CONTENTS ||
kind == VisibilitySymbols::CONTENTS_EXCEPT) {
auto& namePairs = inVisibilitySymbols->names();
bool allowedByType =
(kind == VisibilitySymbols::SYMBOL_ONLY && isSymbolItself) ||
(kind == VisibilitySymbols::ONLY_CONTENTS && !isSymbolItself) ||
(kind == VisibilitySymbols::CONTENTS_EXCEPT && !isSymbolItself);

bool anyMatches = false;
for (auto& namePair : namePairs) {
if (namePair.first == name) {
anyMatches = true;
renameTo = namePair.second;
break;
}
}
if (!allowedByType) return false;

return kind == VisibilitySymbols::CONTENTS_EXCEPT ? !anyMatches : anyMatches;
auto& namePairs = inVisibilitySymbols->names();
bool anyMatches = false;
for (auto& namePair : namePairs) {
if (namePair.first == name) {
anyMatches = true;
renameTo = namePair.second;
break;
}
}

return false;
return kind == VisibilitySymbols::CONTENTS_EXCEPT ? !anyMatches : anyMatches;
};


auto flags = 0;
if (inVisibilitySymbols) flags |= IdAndFlags::PUBLIC;
IdAndFlags::FlagSet excludeNothing;

for (auto& decl : scope->declared()) {
UniqueString renameTo;
if (!allowedByVisibility(decl.first, renameTo)) continue;

auto flags = 0;
if (inVisibilitySymbols) flags |= IdAndFlags::PUBLIC;
if (!allowedByVisibility(decl.first, renameTo, false)) continue;

auto exclude = IdAndFlags::FlagSet::empty();
if (auto borrowed = decl.second.borrow(flags, exclude)) {
toReturn.try_emplace(renameTo, *borrowed);
}
}

// Handle introducing the 'IO' in 'use IO'
if (!scope->name().isEmpty()) {
UniqueString renameTo;
if (allowedByVisibility(scope->name(), renameTo, true)) {
auto symbolItself =
borrowedIdsForScopeSymbol(context, /* flags */ 0, excludeNothing, scope);
if (symbolItself) {
toReturn.try_emplace(renameTo, *symbolItself);
}
}
}

auto resolvedVisStmts = resolveVisibilityStmts(context, scope);
if (!resolvedVisStmts) return QUERY_END(toReturn);

Expand All @@ -1708,9 +1741,7 @@ getSymbolsAvailableInScopeQuery(Context* context,
auto visStmtSymbols =
getSymbolsAvailableInScopeQuery(context, vis.scope(), &vis);
for (auto& pair : visStmtSymbols) {
UniqueString renameTo;
if (!allowedByVisibility(pair.first, renameTo)) continue;
toReturn.try_emplace(renameTo, pair.second);
toReturn.try_emplace(pair.first, pair.second);
}
}

Expand All @@ -1725,10 +1756,14 @@ getSymbolsAvailableInScope(Context* context,

if (scope->autoUsesModules()) {
auto scope = scopeForAutoModule(context);
auto inAuto = getSymbolsAvailableInScopeQuery(context, scope, nullptr);

for (auto& pair : inAuto) {
inScope.try_emplace(pair.first, pair.second);
if (scope) {
// Auto modules might not be loaded in a 'minimal' config.
auto inAuto = getSymbolsAvailableInScopeQuery(context, scope, nullptr);

for (auto& pair : inAuto) {
inScope.try_emplace(pair.first, pair.second);
}
}
}

Expand Down
1 change: 1 addition & 0 deletions frontend/test/resolution/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ comp_unit_test(testFindDecl)
comp_unit_test(testForwarding)
comp_unit_test(testFunctionCalls)
comp_unit_test(testGenericDefaults)
comp_unit_test(testGetSymbolsAvailableInScope)
comp_unit_test(testIf)
comp_unit_test(testInitSemantics)
comp_unit_test(testInteractive)
Expand Down
202 changes: 202 additions & 0 deletions frontend/test/resolution/testGetSymbolsAvailableInScope.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/*
* Copyright 2021-2024 Hewlett Packard Enterprise Development LP
* Other additional copyright holders may be indicated within.
*
* The entirety of this work is licensed under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
*
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#include "test-resolution.h"

#include "chpl/parsing/parsing-queries.h"
#include "chpl/resolution/resolution-queries.h"
#include "chpl/resolution/scope-queries.h"
#include "chpl/uast/Block.h"
#include "chpl/uast/Identifier.h"
#include "chpl/uast/Include.h"
#include "chpl/uast/Module.h"
#include "chpl/uast/Variable.h"

// helper functions

struct NameFinder {
std::set<UniqueString> names;

bool enter(const NamedDecl* named) {
names.insert(named->name());
return true;
}

bool enter(const Identifier* ident) {
names.insert(ident->name());
return true;
}

bool enter(const AstNode* node) {
return true;
}

void exit(const AstNode* node) {}
};

static void checkScopeContents(Context* context, const Module* mod,
std::vector<std::string>&& expected) {
auto scope = scopeForId(context, mod->id());

std::vector<std::string> actual;
for (auto& symPair : getSymbolsAvailableInScope(context, scope)) {
actual.push_back(symPair.first.str());
}
std::sort(actual.begin(), actual.end());
std::sort(expected.begin(), expected.end());

assert(actual.size() == expected.size());
for (size_t i = 0; i < expected.size(); i++) {
assert(actual[i] == expected[i]);
}
}

static void checkScopeContentsViaLookup(Context* context, const Module* mod,
const std::set<UniqueString>& allNames) {
// Verify that looking up identifiers in the scope and asking for
// all identifiers matches up.
//
// The property should be:
//
// for each symbol s,
// lookup(scope, s) nonempty iff s in getSymbolsAvailableInScope(scope)

auto scope = scopeForId(context, mod->id());
auto allSyms = getSymbolsAvailableInScope(context, scope);

for (auto name : allNames) {
// 'int' is visible from all scopes, but we don't list it in allSyms.
if (name == USTR("int")) continue;

auto result = lookupNameInScope(context, scope, {}, name, IDENTIFIER_LOOKUP_CONFIG);

if (result.empty()) {
assert(allSyms.find(name) == allSyms.end());
} else {
assert(allSyms.find(name) != allSyms.end());
}
}
}

static void test1() {
printf("test1\n");
Context ctx;
Context* context = &ctx;

auto path = UniqueString::get(context, "input.chpl");
std::string contents = R""""(
module M {
var x: int;
private proc f1() {}
proc f2() {}
}
module N {
var y: int;
private proc f3() {}
proc f4() {}
}
module O {
var z: int;
private proc f5() {}
proc f6() {}
}
module P {
public use M;
use N;
public use O as O;
}
module Q {
use P;
}
)"""";
setFileText(context, path, contents);

const ModuleVec& vec = parseToplevel(context, path);

// Note: the function under test does not include the name of the module itself,
// and nor does it include builtin names such as 'int'. The former is
// because the intention of 'getSymbolsAvailableInScope' is to match the set
// of identifiers that can be found using 'lookupNameInScope' -- and
// 'lookupNameInScope' does not report the module's own name. The latter is
// an implementation decision, and might need to be updated in the future.

checkScopeContents(context, vec[0], {"x", "f1", "f2"});
checkScopeContents(context, vec[1], {"y", "f3", "f4"});
checkScopeContents(context, vec[2], {"z", "f5", "f6"});
checkScopeContents(context, vec[3], {"x", "y", "z", "f2", "f4", "f6", "N", "O"});
checkScopeContents(context, vec[4], {"x", "z", "f2", "f6", "O", "P"});

NameFinder finder;
for (auto mod : vec) mod->traverse(finder);
for (auto mod : vec) checkScopeContentsViaLookup(context, mod, finder.names);
}

static void test2() {
printf("test2\n");
Context ctx;
Context* context = &ctx;

auto path = UniqueString::get(context, "input.chpl");
std::string contents = R""""(
module M {
var x: int;
private proc f1() {}
proc f2() {}
}
module N {
var y: int;
private proc f3() {}
proc f4() {}
}
module O {
var z: int;
private proc f5() {}
proc f6() {}
}
module P {
public use M only x;
use N only y as yy;
import O.{z, f6 as ff6};
}
)"""";
setFileText(context, path, contents);

const ModuleVec& vec = parseToplevel(context, path);

checkScopeContents(context, vec[0], {"x", "f1", "f2"});
checkScopeContents(context, vec[1], {"y", "f3", "f4"});
checkScopeContents(context, vec[2], {"z", "f5", "f6"});
checkScopeContents(context, vec[3], {"x", "yy", "z", "ff6", "N"});

NameFinder finder;
for (auto mod : vec) mod->traverse(finder);
for (auto mod : vec) checkScopeContentsViaLookup(context, mod, finder.names);
}

int main() {
test1();
test2();
return 0;
}
Loading

0 comments on commit f51e663

Please sign in to comment.