Skip to content

Commit

Permalink
dyno: implement generic initializers (chapel-lang#25026)
Browse files Browse the repository at this point in the history
Closes Cray/chapel-private#6232. Closes
Cray/chapel-private#6109.

This PR fixes various issues in the way of initializing generic types.
There are two broad classes of features that this addresses.

## Compiler-generated initializers
Prior to this, we didn't support compiler-generated initializers for
generic types. This PR moves towards this support by making the
following changes:
* Explicitly introducing a new 'has default' state: 'might have
default'. The reason for this is that fields of generic types may or may
not have default values, which means that we don't quite know the
signature of the initializer until we start instantiating it. But we
need that signature when considering initial candidates! This PR relaxes
the rule to initial mark formals with unknown types to have a 'might
have default' value, allowing missing formals in that case.
* Adjusting function signature instantiate to not assume that a
compiler-generated function on an aggregate type is its type constructor
(since initializers might need instantiations too now). This is largely
a mechanical change; the function is changed to skip the first formal
when creating substitutions (since the first formal is 'this'), and to
populate said formal with the instantiated fields.
* Making the decision that substitutions should only be present in a
type if the corresponding field requires a formal in the type
constructor (re-using the logic for deciding this). The rationale is
simple: if `typeConstructor(a1, ..., an)` is sufficient to construct a
type, then the actuals it accepts are sufficient to fully instantiate
that type. The rest can be figured out given these substitution. Making
this assumption helps remain consistent about when to insert
substitutions, and in this particular case, to avoid pulling
instantiation hints from function resolution into resulting records.
* Exposing the "built type with substitutions" helper from return type
inference, which is needed to properly set the `this` formal of an
instantiated compiler-generated initializer.

While there I,
1. Observed that the we don't emit mismatched candidates when trying to
resolve initializers. This is unfortunate, since we can't read the error
message to figure out why our `new` call is not working. I adjusted the
logic to re-run resolution gathering candidates, which is what's
currently done for regular calls.
2. In looking at the new errors I got that listed the rejected
candidates, I noticed that for dependent functions, the formal type is
printed as "Unknown" (since the initial typed signature's formals are
used for the printing). This is less-than-ideal, so I specialized the
error message in that case to not mention 'UnknownType'.
3. Noticed a bug with type constructors, in which generic `var` fields
that directly depend on `type` fields (e.g., `var varField: typeField`)
are marked for inclusion in the type constructor. This was caused by the
fact that they are technically marked "anytype" as opposed to "unknown
type". To fix this, I adjusted the type constructor code treat "var
AnyType" as "Unknown".

## User-provided initializers
User-provided initializers work fine for records after the previous
section (or maybe even before that, I didn't test `main`), but classes
caused problems. I tracked this down to issues with formal-actual
mismatch in the `this` formal. The failure mode is the following:

When resolving `new R(...)`, we resolve `R`, and convert it to a
`var`-intent receiver. This helps properly call `init` functions, which
accept `ref` or `in` receivers. Next, the can-pass logic proceeds in two
steps: first, it compares the types using `==` for a quick return; then,
it falls back to a "can convert" check. For a generic __record__,
`canPass(R, R)` works, because the initial `==` check succeeds. However,
for classes, the formal is an `in borrowed`, but the actual is a `var
owned`, so `==` fails, and a "conversion check" is performed. However,
the tricky part is that we typically don't allow generic actuals for
non-type formals. Thus, `numeric` is a valid actual type for a `type t`,
but not for a `var x`. As a result, our imaginary generic receiver is
rejected by the more complicated conversion check.

The core conundrum was the following: the 'var R' we pass in is not
_really_ a value; it's more of a "distinguisher" used to select relevant
initializers, since we're creating a new value of that type. We do _not_
want to require it to be concrete, since we might be trying to call a
generic initializer. But we do want to continue disallowing generic
actuals in general. So then: how do we distinguish initializer actuals
that can be generic from the garden-variety non-generic kind?

This PR makes that work using a new `INIT_RECEIVER` _kind_. This made
sense to me because any other way of propagating this information will
make the behavior of `canPass` need to depend on more than just the
formal/actual that's being passed in (it would need additional info to
know if it's a receiver). Based on the call sites of `canPass`, this
information would be relatively difficult to gather and thread through.
Thus, this PR:

* Adds the new `INIT_RECEIVER` intent, for use only with `new` and
`init`. It adjusts the code to handle calls for `new` and `this.init` /
`init`. The former requires a simple adjustment, since we already
override the intent to `VAR` on `main`. Now, we just override it to
`INIT_RECEIVER`. The latter required some fiddling with `CallInfo` to
detect calls to `init` and mark their receivers with `INIT_RECEIVER`
appropriately.
* Note that `INIT_RECEIVER` is an internal intent, and is not user
facing. It only exists while resolving calls, and disappears after a
call to an initializer has been resolved.
* Fixes a bug with `call-init-deinit` that I discovered while testing,
in which we call `=` for nested calls to `this.init`. This isn't
necessary, since we're just invoking another initializer in-place.

Reviewed by @riftEmber -- thanks!

## Testing
- [x] dynotests
- [x] paratest
  • Loading branch information
DanilaFe authored May 15, 2024
2 parents 336827d + c0fb173 commit 3677fa5
Show file tree
Hide file tree
Showing 21 changed files with 999 additions and 344 deletions.
13 changes: 13 additions & 0 deletions frontend/include/chpl/resolution/resolution-queries.h
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,19 @@ types::Type::Genericity getTypeGenericity(Context* context,
types::Type::Genericity getTypeGenericity(Context* context,
types::QualifiedType qt);


/**
Returns true if the field should be included in the type constructor.
In that event, also sets formalType to the type the formal should use.
This is also used to decide if a field needs to be include in a type's
substitutions.
*/
bool shouldIncludeFieldInTypeConstructor(Context* context,
const ID& fieldId,
const types::QualifiedType& fieldType,
types::QualifiedType* formalType = nullptr);

/**
Compute an initial TypedFnSignature for a type constructor for a
particular type. If some fields of `t` are still generic,
Expand Down
36 changes: 27 additions & 9 deletions frontend/include/chpl/resolution/resolution-types.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,25 +84,40 @@ enum struct DefaultsPolicy {
*/
class UntypedFnSignature {
public:
enum DefaultKind {
/** Formals that have default values, like `in x = 10` */
DK_DEFAULT,
/** Formals that do not have default values, like `ref x` */
DK_NO_DEFAULT,
/** Formals that might have a default value. This comes up when working
with generic initializers; whether an initializer's formal has
a default depends on if its type has a default value. But if
the type is unknown -- as in a generic initializer's type signature --
then we don't know if the formal has a default. */
DK_MAYBE_DEFAULT,
};

struct FormalDetail {
UniqueString name;
bool hasDefaultValue = false;
DefaultKind defaultKind = DK_NO_DEFAULT;
const uast::Decl* decl = nullptr;
bool isVarArgs = false;

FormalDetail(UniqueString name,
bool hasDefaultValue,
DefaultKind defaultKind,
const uast::Decl* decl,
bool isVarArgs = false)
: name(name),
hasDefaultValue(hasDefaultValue),
defaultKind(defaultKind),
decl(decl),
isVarArgs(isVarArgs)
{ }
{
CHPL_ASSERT(name != USTR("this") || defaultKind == DK_NO_DEFAULT);
}

bool operator==(const FormalDetail& other) const {
return name == other.name &&
hasDefaultValue == other.hasDefaultValue &&
defaultKind == other.defaultKind &&
decl == other.decl &&
isVarArgs == other.isVarArgs;
}
Expand All @@ -111,7 +126,7 @@ class UntypedFnSignature {
}

size_t hash() const {
return chpl::hash(name, hasDefaultValue, decl, isVarArgs);
return chpl::hash(name, defaultKind, decl, isVarArgs);
}

void stringify(std::ostream& ss, chpl::StringifyKind stringKind) const {
Expand Down Expand Up @@ -309,10 +324,10 @@ class UntypedFnSignature {
return formals_[i].name;
}

/** Return whether the i'th formal has a default value. */
bool formalHasDefault(int i) const {
/** Return whether the i'th formal might have a default value. */
bool formalMightHaveDefault(int i) const {
CHPL_ASSERT(0 <= i && (size_t) i < formals_.size());
return formals_[i].hasDefaultValue;
return formals_[i].defaultKind != DK_NO_DEFAULT;
}

/** Returns the Decl for the i'th formal / field.
Expand Down Expand Up @@ -1210,6 +1225,9 @@ enum PassingFailureReason {
FAIL_CANNOT_CONVERT,
/* An instantiation was needed but is not possible. */
FAIL_CANNOT_INSTANTIATE,
/* We had a generic formal, but the actual did not instantiate it; actual
might be generic. */
FAIL_DID_NOT_INSTANTIATE,
/* A type was used as an argument to a value, or the other way around. */
FAIL_TYPE_VS_NONTYPE,
/* A param value was expected, but a non-param value was given. */
Expand Down
1 change: 1 addition & 0 deletions frontend/include/chpl/types/QualifiedType.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class QualifiedType final {
static const Kind FUNCTION = uast::Qualifier::FUNCTION;
static const Kind PARENLESS_FUNCTION = uast::Qualifier::PARENLESS_FUNCTION;
static const Kind MODULE = uast::Qualifier::MODULE;
static const Kind INIT_RECEIVER = uast::Qualifier::INIT_RECEIVER;

static const char* kindToString(Kind k);

Expand Down
3 changes: 3 additions & 0 deletions frontend/include/chpl/uast/Qualifier.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ enum struct Qualifier {

/** A module */
MODULE,

/** An 'imaginary' actual to 'init''s this to represent the type being constructed. */
INIT_RECEIVER,
};

/** Returns 'true' for qualifiers that are generic such as DEFAULT_INTENT */
Expand Down
28 changes: 10 additions & 18 deletions frontend/lib/resolution/InitResolver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,6 @@ static const Type* receiverTypeFromTfs(const TypedFnSignature* tfs) {
return ret;
}

static const CompositeType* typeToCompType(const Type* type) {
if (auto cls = type->toClassType()) {
return cls->manageableType()->toCompositeType();
} else {
auto ret = type->toCompositeType();
return ret;
}
}

owned<InitResolver>
InitResolver::create(Context* ctx, Resolver& visitor, const Function* fn) {
auto tfs = visitor.typedSignature;
Expand Down Expand Up @@ -91,7 +82,7 @@ bool InitResolver::setupFromType(const Type* type) {
fieldToInitState_.clear();
fieldIdsByOrdinal_.clear();

auto ct = typeToCompType(type);
auto ct = type->getCompositeType();
auto& rf = fieldsForTypeDecl(ctx_, ct, DefaultsPolicy::USE_DEFAULTS);

// If any of the newly-set fields are type or params, setting them
Expand Down Expand Up @@ -228,7 +219,7 @@ void InitResolver::merge(owned<InitResolver>& A, owned<InitResolver>& B) {
}

bool InitResolver::isFinalReceiverStateValid(void) {
auto ctInitial = typeToCompType(initialRecvType_);
auto ctInitial = initialRecvType_->getCompositeType();
auto& rfInitial = fieldsForTypeDecl(ctx_, ctInitial,
DefaultsPolicy::USE_DEFAULTS);
bool ret = true;
Expand Down Expand Up @@ -293,7 +284,7 @@ static const Type* ctFromSubs(Context* context,
}

const Type* InitResolver::computeReceiverTypeConsideringState(void) {
auto ctInitial = typeToCompType(initialRecvType_);
auto ctInitial = initialRecvType_->getCompositeType();

// The non-default fields are used to determine if we need to create
// substitutions. I.e., if a field is concrete even if we ignore defaults,
Expand All @@ -308,7 +299,6 @@ const Type* InitResolver::computeReceiverTypeConsideringState(void) {

auto isValidQtForSubstitutions = [this](const QualifiedType qt) {
if (qt.isUnknown()) return false;
if (!qt.isType() && !qt.isParam()) return false;
return getTypeGenericity(this->ctx_, qt.type()) == Type::CONCRETE;
};

Expand All @@ -320,6 +310,8 @@ const Type* InitResolver::computeReceiverTypeConsideringState(void) {

if (isInitiallyConcrete) continue;

if (!shouldIncludeFieldInTypeConstructor(ctx_, id, qtInitial)) continue;

// TODO: Will need to relax this as we go.
if (isValidQtForSubstitutions(state->qt)) {
subs.insert({id, state->qt});
Expand All @@ -336,7 +328,7 @@ const Type* InitResolver::computeReceiverTypeConsideringState(void) {
// dependently typed, we might be able to compute defaults that
// depend on these prior substitutions.
auto ctIntermediate = ctFromSubs(ctx_, initialRecvType_, ctInitial, subs);
auto& rfIntermediate = fieldsForTypeDecl(ctx_, typeToCompType(ctIntermediate),
auto& rfIntermediate = fieldsForTypeDecl(ctx_, ctIntermediate->getCompositeType(),
DefaultsPolicy::USE_DEFAULTS);

qtForSub = rfIntermediate.fieldType(i);
Expand Down Expand Up @@ -441,7 +433,7 @@ bool InitResolver::implicitlyResolveFieldType(ID id) {
auto state = fieldStateFromId(id);
if (!state || !state->initPointId.isEmpty()) return false;

auto ct = typeToCompType(currentRecvType_);
auto ct = currentRecvType_->getCompositeType();
auto& rf = resolveFieldDecl(ctx_, ct, id, DefaultsPolicy::USE_DEFAULTS);
for (int i = 0; i < rf.numFields(); i++) {
auto id = rf.fieldDeclId(i);
Expand Down Expand Up @@ -493,7 +485,7 @@ bool InitResolver::isMentionOfNodeInLhsOfAssign(const AstNode* node) {
ID InitResolver::fieldIdFromName(UniqueString name) {
if (!isNameOfField(ctx_, name, initialRecvType_)) return ID();
// TODO: Need to replace this as we continue to build it up?
auto ct = typeToCompType(initialRecvType_);
auto ct = initialRecvType_->getCompositeType();
auto ret = parsing::fieldIdWithName(ctx_, ct->id(), name);
return ret;
}
Expand Down Expand Up @@ -575,12 +567,12 @@ bool InitResolver::applyResolvedInitCallToState(const FnCall* node,

CHPL_ASSERT(fn->formalName(0) == USTR("this"));
auto receiverType = fn->formalType(0).type();
auto receiverCompType = typeToCompType(receiverType);
auto receiverCompType = receiverType->getCompositeType();
if (receiverCompType->instantiatedFromCompositeType()) {
receiverCompType = receiverCompType->instantiatedFromCompositeType();
}

auto initialCompType = typeToCompType(initialRecvType_);
auto initialCompType = initialRecvType_->getCompositeType();
if (initialCompType->instantiatedFromCompositeType()) {
initialCompType = initialCompType->instantiatedFromCompositeType();
}
Expand Down
Loading

0 comments on commit 3677fa5

Please sign in to comment.