fix: infer instantiated function types for generic function arguments (closes #30)#63
Merged
Merged
Conversation
…closes #30) Passing a generic function like `id<T>(n: T): T` where a monomorphic function type is expected (e.g. map<T,U>'s `converter: fn(e: T): U` parameter) failed immediately: both FunctionType.IsAssignableTo and TypeSolver.UnifyFunctionTypes bail as soon as the two sides' own TypeParameters counts differ, without ever attempting to specialize the argument from context first. TypeChecker.CheckSubsumption now detects this exact mismatch and, when found, infers the argument's own type parameters from the expected parameter/return shape (reusing TypeInferrer.InferFunctionTypeArguments) and materializes a concrete instantiated FunctionType (0 remaining type parameters) before the normal assignability check runs, validating the inferred types against the argument's own constraints first. Separately, TypeInferrer.MatchFunctionTypes (used to infer the OUTER call's own type parameters, e.g. map's T/U, from its arguments) was naively matching a generic argument's raw uninstantiated shape positionally, silently binding U to id's own still-free type parameter instead of a real type - which is also why the error printed the confusing 'fn(number): T' (T being id's parameter, not map's). It now specializes a generic-typed argument using whatever's already been inferred so far before matching against it, so map(numbers, id) correctly infers as number[], not just something that happens to type-check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Qodana for .NET253 new problems were found
☁️ View the detailed Qodana report Detected 1 dependencyThird-party software listThis page lists the third-party software dependencies used in Loom
Contact Qodana teamContact us at qodana-support@jetbrains.com
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #30. Repro from the issue:
used to throw
Type 'fn<T>(T): T' is not assignable to type 'fn(number): T'.'. It now compiles cleanly, and infers precisely —map(numbers, id)is inferred asnumber[](not just something loose that happens to type-check):let result: number[] = map(numbers, id)succeeds,let bad: string[] = map(numbers, id)correctly fails withType 'number' is not assignable to type 'string'.Root cause: Loom represents a generic function as a plain
FunctionTypewhose ownTypeParameterslist is non-empty (no separate wrapper the wayGenericTypewraps generic classes/interfaces). Two places decide whether one function type is compatible with another —FunctionType.IsAssignableToandTypeSolver.UnifyFunctionTypes— and both bail immediately if the two sides' ownTypeParameters.Countdiffer, without ever trying to specialize the side that still has free type parameters using the other side as context.Fix:
TypeChecker.CheckSubsumption(the argument-vs-expected-type check run for every call argument) now detects this exact mismatch and, when found, infers the argument's own type parameters from the expected parameter/return shape (reusing the existingTypeInferrer.InferFunctionTypeArguments), validates them against the argument's own constraints, and materializes a concrete instantiatedFunctionType(0 remaining type parameters) before the normal assignability check runs.TypeInferrer.MatchFunctionTypes(used to infer the outer call's own type parameters —map'sT/U— from its arguments) was naively matching a generic argument's raw, uninstantiated shape positionally, silently bindingUtoid's own still-free type parameter instead of real information (this is also why the original error printed the confusingfn(number): T—Tthere wasid's own parameter name, notmap's). It now specializes a generic-typed argument using whatever's already been inferred so far (e.g.map'sTis already known to benumberby the timeconverter/idis processed) before matching against it.Test plan
dotnet build— succeeds, no warningsdotnet test— 1822/1822 pass (1817 existing + 5 new)map(numbers, id)infers asnumber[], not a fallback likeunknown[]🤖 Generated with Claude Code