From f1f6afba06ed6fca8eae8ba088761aa10105149e Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Wed, 30 Oct 2024 11:07:08 -0700 Subject: [PATCH 1/5] Handle type query domains (and emit errors when parts are queried) Signed-off-by: Danila Fedorin --- frontend/lib/resolution/Resolver.cpp | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/frontend/lib/resolution/Resolver.cpp b/frontend/lib/resolution/Resolver.cpp index 625b38e5303..541101ed255 100644 --- a/frontend/lib/resolution/Resolver.cpp +++ b/frontend/lib/resolution/Resolver.cpp @@ -3847,8 +3847,34 @@ void Resolver::exit(const uast::Domain* decl) { // Add key or range actuals std::vector actuals; + bool freshDomainQuery = false; for (auto expr : decl->exprs()) { - actuals.emplace_back(byPostorder.byAst(expr).type(), UniqueString()); + auto exprType = byPostorder.byAst(expr).type(); + + // If it's a type query, we may be looking at [?D] (where a Domain node + // is implicitly created in the array expression AST). In that case, + // we want the fully generic domain type. + if (expr->isTypeQuery() && exprType.type() && exprType.type()->isAnyType()) { + freshDomainQuery = true; + break; + } + + actuals.emplace_back(exprType, UniqueString()); + } + + if (freshDomainQuery) { + if (decl->numExprs() > 1 || decl->usedCurlyBraces()) { + // We can only query the whole domain using a type query, so reject + // the domain expression. + context->error(decl, "cannot query part of a domain"); + } + + auto& re = byPostorder.byAst(decl); + auto dt = QualifiedType(QualifiedType::CONST_VAR, genericDomainType); + re.setType(dt); + + // No need to perform the call to chpl__buildDomainExpr etc. + return; } // Add definedConst actual if appropriate From 0b563e5f418d6017bb6bf202714cec8367e3e08c Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Wed, 30 Oct 2024 11:07:57 -0700 Subject: [PATCH 2/5] Treat passing to unknown type as instantiation This is important for cases in which type queries need to be populated. They are only populated from substitutions, but substitutions are not created for completely unknown formals, because prior to this commit, a completely unknown formal can be passed to without instantiating (i.e., without substitution). This commit changes that. Signed-off-by: Danila Fedorin --- frontend/lib/resolution/can-pass.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/lib/resolution/can-pass.cpp b/frontend/lib/resolution/can-pass.cpp index 70c631fafec..ae625782a80 100644 --- a/frontend/lib/resolution/can-pass.cpp +++ b/frontend/lib/resolution/can-pass.cpp @@ -993,7 +993,7 @@ CanPassResult CanPassResult::canPass(Context* context, // when computing an initial candidate, 'b' is unknown // but we should allow passing an argument to it. if (formalT->isUnknownType() && !actualQT.isType()) { - return passAsIs(); + return instantiate(); } // allow unknown qualifier for any type actuals From f161cc4ff0270eb9c3506b67c7542c022caeb061 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Wed, 30 Oct 2024 11:10:41 -0700 Subject: [PATCH 3/5] Reject candidates that fail due to inability to infer type queries After computing a formal type, we traverse the formal AST to find out what the type queries are. This may fail (e.g., if the formal AST doesn't match up with the instantiated type). Thus, the post- query type will be unknown. This is an error, since it indicates that the type queries couldn't be properly made sense of and incorporated into the final type info. In such cases, reject the candidate. A concrete example: formal ?k*?t, actual int. The initial type is unknown; after instantiation, the formal type is int. When resolving queries, we can't figure out how to spit init across ?k*?t, leaving type queries unknown. When re-combining the formal type with query info, ?k*?t is again unknown since the queries were not known. This means we couldn't instantiate ?k*?t with int, and the candidate is rejected. Signed-off-by: Danila Fedorin --- frontend/lib/resolution/resolution-queries.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/lib/resolution/resolution-queries.cpp b/frontend/lib/resolution/resolution-queries.cpp index 50e3d9d7aef..91c8595cc91 100644 --- a/frontend/lib/resolution/resolution-queries.cpp +++ b/frontend/lib/resolution/resolution-queries.cpp @@ -2362,6 +2362,14 @@ ApplicabilityResult instantiateSignature(ResolutionContext* rc, r.byAst(entry.formal()).setType(formalType); } + // We've set up the type queries and re-traversed the formal AST to + // compute the type using these queries. If the formal type is still + // unknown at this point, we couldn't extract the type queries, which + // means the call is ill-formed. + if (qFormalType.isUnknownKindOrType()) { + return ApplicabilityResult::failure(sig, FAIL_CANNOT_INSTANTIATE, entry.formalIdx()); + } + auto checkType = !useType.isUnknown() ? useType : formalType; // With the type and query-aware type known, make sure that they're compatible auto passResult = canPass(context, checkType, qFormalType); From 3812cb8c8fcfc33dbae00bda973d5b124e6bc34f Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Wed, 30 Oct 2024 11:14:50 -0700 Subject: [PATCH 4/5] Handle unknown actuals (needed for out-formals) in builtin functions We have some builtin functions like type/param casting to string, handled by the compiler. They did not expect `null` actual types, but this can happen sometimes (e.g., when the call site allows for the possibility of `out`-formals being matched with the actual). This commit adjusts the code to handle that. Signed-off-by: Danila Fedorin --- frontend/lib/resolution/resolution-queries.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frontend/lib/resolution/resolution-queries.cpp b/frontend/lib/resolution/resolution-queries.cpp index 91c8595cc91..74659733d24 100644 --- a/frontend/lib/resolution/resolution-queries.cpp +++ b/frontend/lib/resolution/resolution-queries.cpp @@ -3754,6 +3754,15 @@ static bool resolveFnCallSpecial(Context* context, // TODO: .borrow() // TODO: chpl__coerceCopy + // Sometimes, actual types can be unknown since we are checking for 'out' + // intent. No special functions here use the 'out' intent, so in this case, + // return false. + for (auto& actual : ci.actuals()) { + if (actual.type().isUnknown()) { + return false; + } + } + // special casts including explicit param casts are resolved here if (ci.isOpCall() && ci.name() == USTR(":")) { auto srcQt = ci.actual(0).type(); From 9d0d5e53a53b65d5b327553f0184b3b155e01347 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Wed, 30 Oct 2024 11:16:15 -0700 Subject: [PATCH 5/5] Skip unknown arguments when building ranges. Resolving ranges boils down to resolving a function call. Dyno skips resolving normal function calls under certain conditions (e.g., if the argument types could not be inferred). However, it doesn't do so for ranges, which made it possible for 'Unknown' or otherwise invalid actuals to slip into the call resolution process. This commit adjusts the resolver to re-use the skipping logic. Signed-off-by: Danila Fedorin --- frontend/lib/resolution/Resolver.cpp | 193 ++++++++++++++------------- 1 file changed, 102 insertions(+), 91 deletions(-) diff --git a/frontend/lib/resolution/Resolver.cpp b/frontend/lib/resolution/Resolver.cpp index 541101ed255..109e0a847e4 100644 --- a/frontend/lib/resolution/Resolver.cpp +++ b/frontend/lib/resolution/Resolver.cpp @@ -3768,6 +3768,89 @@ void Resolver::exit(const TupleDecl* decl) { exitScope(decl); } +static SkipCallResolutionReason +shouldSkipCallResolution(Resolver* rv, const uast::AstNode* callLike, + std::vector actualAsts, + const CallInfo& ci) { + Context* context = rv->context; + SkipCallResolutionReason skip = NONE; + auto& byPostorder = rv->byPostorder; + + if (callLike->isTuple()) return skip; + + int actualIdx = 0; + for (const auto& actual : ci.actuals()) { + ID toId; // does the actual refer directly to a particular variable? + const AstNode* actualAst = actualAsts[actualIdx]; + if (actualAst != nullptr && byPostorder.hasAst(actualAst)) { + toId = byPostorder.byAst(actualAst).toId(); + } + QualifiedType qt = actual.type(); + const Type* t = qt.type(); + + auto formalAst = toId.isEmpty() ? nullptr : parsing::idToAst(context, toId); + bool isNonOutFormal = formalAst != nullptr && + formalAst->isFormal() && + formalAst->toFormal()->intent() != Formal::Intent::OUT; + + if (t != nullptr && t->isErroneousType()) { + // always skip if there is an ErroneousType + skip = ERRONEOUS_ACT; + } else if (!toId.isEmpty() && !isNonOutFormal && + qt.kind() != QualifiedType::PARAM && + qt.kind() != QualifiedType::TYPE && + qt.isRef() == false) { + // don't skip because it could be initialized with 'out' intent, + // but not for non-out formals because they can't be split-initialized. + } else if (actualAst->isTypeQuery() && ci.calledType().isType()) { + // don't skip for type queries in type constructors + } else { + if (qt.isParam() && qt.param() == nullptr) { + skip = UNKNOWN_PARAM; + } else if (qt.isUnknown()) { + skip = UNKNOWN_ACT; + } else if (t != nullptr && qt.kind() != QualifiedType::INIT_RECEIVER) { + // For initializer calls, allow generic formals using the above + // condition; this way, 'this.init(..)' while 'this' is generic + // should be fine. + + auto g = getTypeGenericity(context, t); + bool isBuiltinGeneric = (g == Type::GENERIC && + (t->isAnyType() || t->isBuiltinType())); + if (qt.isType() && isBuiltinGeneric && rv->substitutions == nullptr) { + skip = GENERIC_TYPE; + } else if (!qt.isType() && g != Type::CONCRETE) { + skip = GENERIC_VALUE; + } + } + } + + // Don't skip for type constructors, except due to unknown params. + if (skip != UNKNOWN_PARAM && ci.calledType().isType()) { + skip = NONE; + } + + // Do not skip primitive calls that accept a generic type, since they + // may be valid. + if (skip == GENERIC_TYPE && callLike->toPrimCall()) { + skip = NONE; + } + + if (skip) { + break; + } + actualIdx++; + } + + // Don't try to resolve calls to '=' until later + if (ci.isOpCall() && ci.name() == USTR("=")) { + skip = OTHER_REASON; + } + + return skip; +} + + bool Resolver::enter(const Range* range) { return true; } @@ -3798,23 +3881,37 @@ void Resolver::exit(const Range* range) { const char* function = functions[boundType]; std::vector actuals; + std::vector actualAsts; if (range->lowerBound()) { actuals.emplace_back(/* type */ byPostorder.byAst(range->lowerBound()).type(), /* byName */ UniqueString()); + actualAsts.push_back(range->lowerBound()); } if (range->upperBound()) { actuals.emplace_back(/* type */ byPostorder.byAst(range->upperBound()).type(), /* byName */ UniqueString()); + actualAsts.push_back(range->upperBound()); } + + auto ci = CallInfo(/* name */ UniqueString::get(context, function), /* calledType */ QualifiedType(), /* isMethodCall */ false, /* hasQuestionArg */ false, /* isParenless */ false, actuals); - auto scope = scopeStack.back(); - auto inScopes = CallScopeInfo::forNormalCall(scope, poiScope); - auto c = resolveGeneratedCall(context, range, ci, inScopes); - handleResolvedCall(byPostorder.byAst(range), range, ci, c); + + // Skip calls when the bounds are unknown to avoid putting function resolution + // into an awkward position. + auto skip = shouldSkipCallResolution(this, range, actualAsts, ci); + if (!skip) { + auto scope = scopeStack.back(); + auto inScopes = CallScopeInfo::forNormalCall(scope, poiScope); + auto c = resolveGeneratedCall(context, range, ci, inScopes); + handleResolvedCall(byPostorder.byAst(range), range, ci, c); + } else { + auto& r = byPostorder.byAst(range); + r.setType(QualifiedType()); + } } bool Resolver::enter(const uast::Domain* decl) { @@ -4019,89 +4116,6 @@ static const Type* getGenericType(Context* context, const Type* recv) { return gen; } -static SkipCallResolutionReason -shouldSkipCallResolution(Resolver* rv, const uast::Call* call, - std::vector actualAsts, - ID moduleScopeId, - const CallInfo& ci) { - Context* context = rv->context; - SkipCallResolutionReason skip = NONE; - auto& byPostorder = rv->byPostorder; - - if (call->isTuple()) return skip; - - int actualIdx = 0; - for (const auto& actual : ci.actuals()) { - ID toId; // does the actual refer directly to a particular variable? - const AstNode* actualAst = actualAsts[actualIdx]; - if (actualAst != nullptr && byPostorder.hasAst(actualAst)) { - toId = byPostorder.byAst(actualAst).toId(); - } - QualifiedType qt = actual.type(); - const Type* t = qt.type(); - - auto formalAst = toId.isEmpty() ? nullptr : parsing::idToAst(context, toId); - bool isNonOutFormal = formalAst != nullptr && - formalAst->isFormal() && - formalAst->toFormal()->intent() != Formal::Intent::OUT; - - if (t != nullptr && t->isErroneousType()) { - // always skip if there is an ErroneousType - skip = ERRONEOUS_ACT; - } else if (!toId.isEmpty() && !isNonOutFormal && - qt.kind() != QualifiedType::PARAM && - qt.kind() != QualifiedType::TYPE && - qt.isRef() == false) { - // don't skip because it could be initialized with 'out' intent, - // but not for non-out formals because they can't be split-initialized. - } else if (actualAst->isTypeQuery() && ci.calledType().isType()) { - // don't skip for type queries in type constructors - } else { - if (qt.isParam() && qt.param() == nullptr) { - skip = UNKNOWN_PARAM; - } else if (qt.isUnknown()) { - skip = UNKNOWN_ACT; - } else if (t != nullptr && qt.kind() != QualifiedType::INIT_RECEIVER) { - // For initializer calls, allow generic formals using the above - // condition; this way, 'this.init(..)' while 'this' is generic - // should be fine. - - auto g = getTypeGenericity(context, t); - bool isBuiltinGeneric = (g == Type::GENERIC && - (t->isAnyType() || t->isBuiltinType())); - if (qt.isType() && isBuiltinGeneric && rv->substitutions == nullptr) { - skip = GENERIC_TYPE; - } else if (!qt.isType() && g != Type::CONCRETE) { - skip = GENERIC_VALUE; - } - } - } - - // Don't skip for type constructors, except due to unknown params. - if (skip != UNKNOWN_PARAM && ci.calledType().isType()) { - skip = NONE; - } - - // Do not skip primitive calls that accept a generic type, since they - // may be valid. - if (skip == GENERIC_TYPE && call->toPrimCall()) { - skip = NONE; - } - - if (skip) { - break; - } - actualIdx++; - } - - // Don't try to resolve calls to '=' until later - if (ci.isOpCall() && ci.name() == USTR("=")) { - skip = OTHER_REASON; - } - - return skip; -} - static const bool& warnForMissingIterKindEnum(Context* context, const AstNode* astForErr) { QUERY_BEGIN(warnForMissingIterKindEnum, context, astForErr); @@ -4308,10 +4322,7 @@ void Resolver::handleCallExpr(const uast::Call* call) { CallScopeInfo::forNormalCall(scope, poiScope) : CallScopeInfo::forQualifiedCall(context, moduleScopeId, scope, poiScope); - auto skip = shouldSkipCallResolution(this, call, actualAsts, - moduleScopeId, - ci); - + auto skip = shouldSkipCallResolution(this, call, actualAsts, ci); if (!skip) { ResolvedExpression& r = byPostorder.byAst(call); QualifiedType receiverType = methodReceiverType();