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

Track recursive callback-covariance calls to compareSignaturesRelated #19078

Closed

Conversation

sandersn
Copy link
Member

Previously, compareSignaturesRelated would try to relate (parameters of) callback parameters covariantly without checking whether the two signatures were already being checked. This led to an infinite recursion when checking recursive types with callbacks:

interface Foo<T> { (bar: Bar<T>): void }
type Bar<T> = (foo: Foo<T>) => Foo<T>
declare function foo<T>(bar: Bar<T>): void
declare var bar: Bar<{}>
bar = foo

The fix is to track which callbacks are already being compared. The mechanism is per-call to compareSignaturesRelated, so it only stops recursions on the callback-covariance code path. However, the maybe cache check in recursiveTypeRelatedTo will catch infinite recursions on the usual code path through isRelatedTo.

Fixes #18277

Previously, `compareSignaturesRelated` would try to relate (parameters
of) callback parameters covariantly without checking whether the two
signatures were already being checked. This led to an infinite recursion
when checking recursive types with callbacks:

```ts
interface Foo<T> { (bar: Bar<T>): void }
type Bar<T> = (foo: Foo<T>) => Foo<T>
declare function foo<T>(bar: Bar<T>): void
declare var bar: Bar<{}>
bar = foo
```

The fix is to track which callbacks are already being compared. The
mechanism is per-call to compareSignaturesRelated, so it only stops
recursions on the callback-covariance code path. However, the maybe
cache check in recursiveTypeRelatedTo will catch infinite recursions on
the usual code path through isRelatedTo.

Fixes #18277
@@ -8521,7 +8521,7 @@ namespace ts {
target: Signature,
ignoreReturnTypes: boolean): boolean {
return compareSignaturesRelated(source, target, CallbackCheck.None, ignoreReturnTypes, /*reportErrors*/ false,
/*errorReporter*/ undefined, compareTypesAssignable) !== Ternary.False;
/*errorReporter*/ undefined, compareTypesAssignable, createMap<true>()) !== Ternary.False;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't you just pass a type comparer function that checks the map? That way you could keep all of the logic right here and not have to add another parameter to compareSignaturesRelated.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried this but because compareTypes isn't actually called in the tight recursive loop of compareSignaturesRelated, I have to put the whole decision inside the type comparer:

        function createRecursiveSignatureTypeComparer(compareTypes: TypeComparer, callbackCheck: CallbackCheck, target: Signature, errorReporter: ErrorReporter): TypeComparer {
            const previousCallbacks = createMap<true>();
            const kind = target.declaration ? target.declaration.kind : SyntaxKind.Unknown;
            const strictVariance = !callbackCheck && strictFunctionTypes && kind !== SyntaxKind.MethodDeclaration &&
                kind !== SyntaxKind.MethodSignature && kind !== SyntaxKind.Constructor;
            return (source, target, reportErrors) => {
                const sourceSig = getSingleCallSignature(getNonNullableType(source));
                const targetSig = getSingleCallSignature(getNonNullableType(target));
                // In order to ensure that any generic type Foo<T> is at least co-variant with respect to T no matter
                // how Foo uses T, we need to relate parameters bi-variantly (given that parameters are input positions,
                // they naturally relate only contra-variantly). However, if the source and target parameters both have
                // function types with a single call signature, we known we are relating two callback parameters. In
                // that case it is sufficient to only relate the parameters of the signatures co-variantly because,
                // similar to return values, callback parameters are output positions. This means that a Promise<T>,
                // where T is used only in callback parameter positions, will be co-variant (as opposed to bi-variant)
                // with respect to T.
                let callbacks = sourceSig && targetSig && !sourceSig.typePredicate && !targetSig.typePredicate &&
                    (getFalsyFlags(source) & TypeFlags.Nullable) === (getFalsyFlags(target) & TypeFlags.Nullable);
                if (callbacks) {
                    const key = getRelationKey(source, target, /*relation*/ undefined);
                    if (!previousCallbacks.has(key)) {
                        previousCallbacks.set(key, true);
                        return compareSignaturesRelated(targetSig, sourceSig, strictVariance ? CallbackCheck.Strict : CallbackCheck.Bivariant, /*ignoreReturnTypes*/ false, reportErrors, errorReporter, compareTypes);
                    }
                }
                return !callbackCheck && !strictVariance && compareTypes(source, target, /*reportErrors*/ false) || compareTypes(target, source, reportErrors);
            };
        }

Now the inner loop of parameter checking is clean:

            for (let i = 0; i < checkCount; i++) {
                const sourceType = i < sourceMax ? getTypeOfParameter(sourceParams[i]) : getRestTypeOfSignature(source);
                const targetType = i < targetMax ? getTypeOfParameter(targetParams[i]) : getRestTypeOfSignature(target);
                const related = compareTypes(sourceType, targetType, reportErrors);
                if (!related) {
                    if (reportErrors) {
                        errorReporter(Diagnostics.Types_of_parameters_0_and_1_are_incompatible,
                            symbolName(sourceParams[i < sourceMax ? i : sourceMax]),
                            symbolName(targetParams[i < targetMax ? i : targetMax]));
                    }
                    return Ternary.False;
                }
                result &= related;
            }

But I think this part of the algorithm reads better in situ, and the move actually requires more code churn at the call sites of compareSignaturesRelatedTo because createRecursiveSignatureTypeComparer has so many parameters.

To help the code churn at call sites, I could instead just make a compareSignaturesRelated/compareSignaturesRelatedWorker pair in order to hide the creation of the map.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out there's an even simpler fix. I have put it up in #19107.

@sandersn sandersn closed this Oct 11, 2017
@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
@jakebailey jakebailey deleted the track-recursive-calls-to-compareSignaturesRelated branch November 7, 2022 17:31
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Stack overflow while type checking generic, mutually-defined function interfaces
2 participants