Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add split-init error when uninitialized var used as actual #26497

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ ERROR_CLASS(MultipleEnumElems, const uast::AstNode*, chpl::UniqueString, const u
ERROR_CLASS(MultipleInheritance, const uast::Class*, const uast::AstNode*, const uast::AstNode*)
ERROR_CLASS(MultipleQuestionArgs, const uast::FnCall*, const uast::AstNode*, const uast::AstNode*)
ERROR_CLASS(NestedClassFieldRef, const uast::TypeDecl*, const uast::TypeDecl*, const uast::AstNode*, ID)
ERROR_CLASS(NoMatchingCandidates, const uast::AstNode*, resolution::CallInfo, std::vector<resolution::ApplicabilityResult>)
ERROR_CLASS(NoMatchingCandidates, const uast::AstNode*, resolution::CallInfo, std::vector<resolution::ApplicabilityResult>, std::vector<resolution::FormalActual>, std::vector<const uast::VarLikeDecl*>)
ERROR_CLASS(NonIterable, const uast::AstNode*, const uast::AstNode*, types::QualifiedType, std::vector<std::tuple<uast::Function::IteratorKind, chpl::resolution::TheseResolutionResult>>)
ERROR_CLASS(NoMatchingEnumValue, const uast::AstNode*, const types::EnumType*, types::QualifiedType)
ERROR_CLASS(NotInModule, const uast::Dot*, ID, UniqueString, ID, bool)
Expand Down
75 changes: 64 additions & 11 deletions frontend/lib/resolution/Resolver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,54 @@ static bool isCallToPtr(const AstNode* formalTypeExpr) {
return false;
}

// helper to gather bad actuals and report NoMatchingCandidates error
static void
handleRejectedCandidates(Context* context,
ResolutionResultByPostorderID& byPostorder,
std::vector<ApplicabilityResult>& rejected,
const resolution::CallInfo& ci,
const uast::Call*& call,
const std::vector<const uast::AstNode*>& actualAsts) {
// By performing some processing in the resolver, we can issue a nicer error
// explaining why each candidate was rejected.
std::vector<resolution::FormalActual> badPasses;
std::vector<const uast::VarLikeDecl*> actualDecls;
badPasses.resize(rejected.size());
actualDecls.resize(rejected.size());
// check each rejected candidate for uninitialized actuals
for (size_t i = 0; i < rejected.size(); i++) {
auto &candidate = rejected[i];
if (candidate.reason() == resolution::FAIL_CANNOT_PASS &&
/* skip computing the formal-actual map because it will go poorly
with an unknown formal. */
candidate.formalReason() != resolution::FAIL_UNKNOWN_FORMAL_TYPE) {
auto fn = candidate.initialForErr();
resolution::FormalActualMap fa(fn, ci);
auto& badPass = fa.byFormalIdx(candidate.formalIdx());
badPasses[i] = badPass;
const uast::AstNode *actualExpr = nullptr;
const uast::VarLikeDecl *actualDecl = nullptr;
if (call && 0 <= badPass.actualIdx() &&
(size_t)badPass.actualIdx() < actualAsts.size()) {
actualExpr = actualAsts[badPass.actualIdx()];
}
// look for a definition point of the actual for error reporting of
// uninitialized vars typically in the case of bad split-initialization
if (actualExpr && actualExpr->isIdentifier()) {
auto &resolvedExpr = byPostorder.byAst(actualExpr->toIdentifier());
if (auto id = resolvedExpr.toId()) {
auto var = parsing::idToAst(context, id);
// should put a nullptr if not a VarLikeDecl
actualDecl = var->toVarLikeDecl();
}
}
actualDecls[i] = actualDecl;
}
}
CHPL_ASSERT(badPasses.size() == rejected.size() && rejected.size() == actualDecls.size());
CHPL_REPORT(context, NoMatchingCandidates, call, ci, rejected, badPasses, actualDecls);
}

static void varArgTypeQueryError(Context* context,
const AstNode* node,
ResolvedExpression& result) {
Expand Down Expand Up @@ -1804,9 +1852,11 @@ Resolver::issueErrorForFailedCallResolution(const uast::AstNode* astForErr,
context->error(astForErr, "Cannot resolve call to '%s': ambiguity",
ci.name().c_str());
} else {
std::vector<const uast::VarLikeDecl*> uninitializedActuals;
std::vector<resolution::FormalActual> faPairs;
// could not find a most specific candidate
std::vector<ApplicabilityResult> rejected;
CHPL_REPORT(context, NoMatchingCandidates, astForErr, ci, rejected);
CHPL_REPORT(context, NoMatchingCandidates, astForErr, ci, rejected, faPairs, uninitializedActuals);
}
} else {
context->error(astForErr, "Cannot establish type for call expression");
Expand Down Expand Up @@ -1928,6 +1978,7 @@ void Resolver::handleResolvedCallPrintCandidates(ResolvedExpression& r,
const CallScopeInfo& inScopes,
const QualifiedType& receiverType,
const CallResolutionResult& c,
std::vector<const uast::AstNode*>& actualAsts,
optional<ActionAndId> actionAndId) {
bool wasCallGenerated = (bool) actionAndId;
CHPL_ASSERT(!wasCallGenerated || receiverType.isUnknown());
Expand All @@ -1947,9 +1998,8 @@ void Resolver::handleResolvedCallPrintCandidates(ResolvedExpression& r,
}

if (!rejected.empty()) {
// There were candidates but we threw them out. We can issue a nicer
// error explaining why each candidate was rejected.
CHPL_REPORT(context, NoMatchingCandidates, call, ci, rejected);
// There were candidates but we threw them out. Report on those.
handleRejectedCandidates(context, byPostorder, rejected, ci, call, actualAsts);
return;
}
}
Expand Down Expand Up @@ -2295,13 +2345,14 @@ bool Resolver::resolveSpecialNewCall(const Call* call) {
bool isMethodCall = true;
const AstNode* questionArg = nullptr;
std::vector<CallInfoActual> actuals;
std::vector<const uast::AstNode*> actualAsts;

// Prepare receiver.
auto receiverInfo = CallInfoActual(calledType, USTR("this"));
actuals.push_back(std::move(receiverInfo));

// Remaining actuals.
prepareCallInfoActuals(call, actuals, questionArg);
prepareCallInfoActuals(call, actuals, questionArg, &actualAsts);
CHPL_ASSERT(!questionArg);

// The 'new' will produce an 'init' call as a side effect.
Expand All @@ -2315,8 +2366,9 @@ bool Resolver::resolveSpecialNewCall(const Call* call) {

// note: the resolution machinery will get compiler generated candidates
auto crr = resolveGeneratedCall(context, call, ci, inScopes);
handleResolvedCallPrintCandidates(re, call, ci, inScopes, QualifiedType(), crr,
{ { AssociatedAction::NEW_INIT, call->id() } });
optional<ActionAndId> action = { { AssociatedAction::NEW_INIT, call->id() } };
handleResolvedCallPrintCandidates(re, call, ci, inScopes, QualifiedType(),
crr, actualAsts, action);


// there should be one or zero applicable candidates
Expand Down Expand Up @@ -2471,7 +2523,7 @@ bool Resolver::resolveSpecialKeywordCall(const Call* call) {
DomainType::getDefaultDistType(context), UniqueString());
actuals.push_back(std::move(defaultDistArg));
// Remaining given args from domain() call as written
prepareCallInfoActuals(call, actuals, questionArg);
prepareCallInfoActuals(call, actuals, questionArg, /*actualAsts*/ nullptr);
CHPL_ASSERT(!questionArg);

auto ci =
Expand Down Expand Up @@ -4195,11 +4247,12 @@ bool Resolver::enter(const Call* call) {

void Resolver::prepareCallInfoActuals(const Call* call,
std::vector<CallInfoActual>& actuals,
const AstNode*& questionArg) {
const AstNode*& questionArg,
std::vector<const uast::AstNode*>* actualAsts) {
CallInfo::prepareActuals(context, call, byPostorder,
/* raiseErrors */ true,
actuals, questionArg,
/* actualAsts */ nullptr);
actualAsts);
}

static const Type* getGenericType(Context* context, const Type* recv) {
Expand Down Expand Up @@ -4465,7 +4518,7 @@ void Resolver::handleCallExpr(const uast::Call* call) {
rejected);

// save the most specific candidates in the resolution result for the id
handleResolvedCallPrintCandidates(r, call, ci, inScopes, receiverType, c);
handleResolvedCallPrintCandidates(r, call, ci, inScopes, receiverType, c, actualAsts);

// handle type inference for variables split-inited by 'out' formals
adjustTypesForOutFormals(ci, actualAsts, c.mostSpecific());
Expand Down
4 changes: 3 additions & 1 deletion frontend/lib/resolution/Resolver.h
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ struct Resolver {
const CallScopeInfo& inScopes,
const types::QualifiedType& receiverType,
const CallResolutionResult& c,
std::vector<const uast::AstNode*>& actualAsts,
optional<ActionAndId> associatedActionAndId = {});

// If the variable with the passed ID has unknown or generic type,
Expand Down Expand Up @@ -580,7 +581,8 @@ struct Resolver {
// includes special handling for operators and tuple literals
void prepareCallInfoActuals(const uast::Call* call,
std::vector<CallInfoActual>& actuals,
const uast::AstNode*& questionArg);
const uast::AstNode*& questionArg,
std::vector<const uast::AstNode*>* actualAsts);

// prepare a CallInfo by inspecting the called expression and actuals
CallInfo prepareCallInfoNormalCall(const uast::Call* call);
Expand Down
34 changes: 26 additions & 8 deletions frontend/lib/resolution/resolution-error-classes-list.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1010,25 +1010,24 @@ void ErrorNoMatchingCandidates::write(ErrorWriterBase& wr) const {
auto call = node->toCall();
auto& ci = std::get<resolution::CallInfo>(info_);
auto& rejected = std::get<std::vector<resolution::ApplicabilityResult>>(info_);
auto& formalActuals = std::get<std::vector<resolution::FormalActual>>(info_);
auto& actualDecls = std::get<std::vector<const uast::VarLikeDecl*>>(info_);

wr.heading(kind_, type_, node, "unable to resolve call to '", ci.name(), "': no matching candidates.");
wr.code(node);

unsigned int printCount = 0;
unsigned int iterCount = 0;
static const unsigned int maxPrintCount = 2;
for (auto& candidate : rejected) {
if (printCount == maxPrintCount) break;
printCount++;

auto reason = candidate.reason();
wr.message("");
if (reason == resolution::FAIL_CANNOT_PASS &&
/* skip printing detailed info_ here because computing the formal-actual
map will go poorly with an unknown formal. */
candidate.formalReason() != resolution::FAIL_UNKNOWN_FORMAL_TYPE) {
if (formalActuals[iterCount].formalIdx() != -1) {
auto fn = candidate.initialForErr();
resolution::FormalActualMap fa(fn, ci);
auto badPass = fa.byFormalIdx(candidate.formalIdx());
auto badPass = formalActuals[iterCount];
auto formalDecl = badPass.formal();
const uast::AstNode* actualExpr = nullptr;
if (call && 0 <= badPass.actualIdx() && badPass.actualIdx() < call->numActuals()) {
Expand All @@ -1044,19 +1043,37 @@ void ErrorNoMatchingCandidates::write(ErrorWriterBase& wr) const {
} else if (formalDecl->isTupleDecl()) {
formalName = "'" + buildTupleDeclName(formalDecl->toTupleDecl()) + "'";
}

bool actualPrinted = false;
auto offendingActual = actualDecls[iterCount];
if (badPass.formalType().isUnknown()) {
// The formal type can be unknown in an initial instantiation if it
// depends on the previous formals' types. In that case, don't print it
// and say something nicer.
wr.message("The instantiated type of formal ", formalName,
" does not allow actuals of type '", badPass.actualType().type(), "'.");
} else if (badPass.actualType().isUnknown() &&
offendingActual &&
!offendingActual->initExpression() &&
!offendingActual->typeExpression()) {
auto formalKind = badPass.formalType().kind();
auto actualName = "'" + actualExpr->toIdentifier()->name().str() + "'";
wr.note(actualExpr->id(), "The actual ", actualName,
" expects to be split-initialized because it is declared without a type or initialization expression here:");
wr.code(offendingActual, { offendingActual });
wr.message("The call to '", ci.name() ,"' occurs before any valid initialization points:");
wr.code(actualExpr, { actualExpr });
actualPrinted =true;
wr.message("The call to '", ci.name(), "' cannot initialize ",
actualName,
" because only 'out' formals can be used to split-initialize. However, ",
actualName, " is passed to formal ", formalName, " which has intent '", formalKind, "'.");

} else {
wr.message("The formal ", formalName, " expects ", badPass.formalType(),
", but the actual was ", badPass.actualType(), ".");
}

if (actualExpr) {
if (!actualPrinted && actualExpr) {
wr.code(actualExpr, { actualExpr });
}

Expand Down Expand Up @@ -1117,6 +1134,7 @@ void ErrorNoMatchingCandidates::write(ErrorWriterBase& wr) const {
}
wr.code(candidate.idForErr());
}
iterCount++;
}

if (printCount < rejected.size()) {
Expand Down
5 changes: 5 additions & 0 deletions test/errors/resolution/noMatchingCandidates.1.good
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
noMatchingCandidates.chpl:6: error: unable to resolve call to 'fn': no matching candidates
noMatchingCandidates.chpl:2: note: the following candidate didn't match because an actual couldn't be passed to a formal
noMatchingCandidates.chpl:6: note: The actual 'x' expects to be split-initialized because it is declared without a type or initialization expression here
noMatchingCandidates.chpl:14: error: unable to resolve call to 'fn': no matching candidates
noMatchingCandidates.chpl:10: note: the following candidate didn't match because an actual couldn't be passed to a formal
45 changes: 45 additions & 0 deletions test/errors/resolution/noMatchingCandidates.2.good
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
─── error in noMatchingCandidates.chpl:6 [NoMatchingCandidates] ───
Unable to resolve call to 'fn': no matching candidates.
|
6 | fn(x);
|

The following candidate didn't match because an actual couldn't be passed to a formal:
|
2 | proc fn(const arg) {
| ⎺⎺⎺⎺⎺⎺⎺⎺⎺
3 | arg;
4 | }
|
The actual 'x' expects to be split-initialized because it is declared without a type or initialization expression here:
|
5 | var x;
| ⎺
|
The call to 'fn' occurs before any valid initialization points:
|
6 | fn(x);
| ⎺
|
The call to 'fn' cannot initialize 'x' because only 'out' formals can be used to split-initialize. However, 'x' is passed to formal 'arg' which has intent 'const'.

─── error in noMatchingCandidates.chpl:14 [NoMatchingCandidates] ───
Unable to resolve call to 'fn': no matching candidates.
|
14 | fn(x);
|

The following candidate didn't match because an actual couldn't be passed to a formal:
|
10 | proc fn(arg:string) {
| ⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
11 | arg;
12 | }
|
The formal 'arg' expects a value of type 'string', but the actual was a value of type 'int(64)'.
|
14 | fn(x);
| ⎺
|
Formals with kind 'const ref' expect the actual to be a subtype, but 'int(64)' is not a subtype of 'string'.

35 changes: 35 additions & 0 deletions test/errors/resolution/noMatchingCandidates.chpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{ // expects split-init
proc fn(const arg) {
arg;
}
var x;
fn(x);
x=5;
}
{ // type mismatch
proc fn(arg:string) {
arg;
}
var x=5;
fn(x);
}
// Tests needed:
// call was parenful but method is parenless

// call was parenless but method was parenful

// bad where clause

// bad vararg count

// star tuple mismatches

// tuple size mismatches

// The 'ref' intent requires the formal and actual types to match exactly

// bad subtype

// incompatible manager

// incompatible nilability