Skip to content

fix: infer instantiated function types for generic function arguments (closes #30)#63

Merged
R-unic merged 1 commit into
masterfrom
fix/infer-instantiated-function-types
Jul 24, 2026
Merged

fix: infer instantiated function types for generic function arguments (closes #30)#63
R-unic merged 1 commit into
masterfrom
fix/infer-instantiated-function-types

Conversation

@R-unic

@R-unic R-unic commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #30. Repro from the issue:

let numbers = [1, 2, 3, 4];
fn map<T, U>(array: T[], converter: fn(e: T): U): U[] {
  let new_array: U[mut] = mut [];
  for v : array
    new_array.push(converter(v));
  return new_array;
}

fn id<T>(n: T) -> n;
print(map(numbers, id));

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 as number[] (not just something loose that happens to type-check): let result: number[] = map(numbers, id) succeeds, let bad: string[] = map(numbers, id) correctly fails with Type 'number' is not assignable to type 'string'.

Root cause: Loom represents a generic function as a plain FunctionType whose own TypeParameters list is non-empty (no separate wrapper the way GenericType wraps generic classes/interfaces). Two places decide whether one function type is compatible with another — FunctionType.IsAssignableTo and TypeSolver.UnifyFunctionTypes — and both bail immediately if the two sides' own TypeParameters.Count differ, 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 existing TypeInferrer.InferFunctionTypeArguments), validates them against the argument's own constraints, and materializes a concrete instantiated FunctionType (0 remaining type parameters) before the normal assignability check runs.
  • Separately, TypeInferrer.MatchFunctionTypes (used to infer the outer call's own type parameters — 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 real information (this is also why the original error printed the confusing fn(number): TT there was id's own parameter name, not map's). It now specializes a generic-typed argument using whatever's already been inferred so far (e.g. map's T is already known to be number by the time converter/id is processed) before matching against it.

Test plan

  • dotnet build — succeeds, no warnings
  • dotnet test — 1822/1822 pass (1817 existing + 5 new)
  • Verified the exact issue repro compiles cleanly end-to-end via the CLI
  • Verified precision: map(numbers, id) infers as number[], not a fallback like unknown[]
  • Verified a case that should still correctly fail: a generic argument whose constraint can't be satisfied by the inferred type produces a sensible type-mismatch error instead of silently accepting it
  • Verified generic functions used as plain (non-argument) values are unaffected

🤖 Generated with Claude Code

…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>
@github-actions

Copy link
Copy Markdown

Qodana for .NET

253 new problems were found

Inspection name Severity Problems
Auto-property accessor is never used (private accessibility) 🔶 Warning 6
Auto-property accessor is never used (non-private accessibility) 🔶 Warning 5
Non-accessed positional property (non-private accessibility) 🔶 Warning 2
Redundant name qualifier 🔶 Warning 1
Type member is never used (private accessibility) 🔶 Warning 1
Unused local variable 🔶 Warning 1
Type member is never used (non-private accessibility) ◽️ Notice 94
Use preferred style of 'new' expression when created type is not evident ◽️ Notice 83
Member can be made private (non-private accessibility) ◽️ Notice 20
Invert 'if' statement to reduce nesting ◽️ Notice 5
Class is never instantiated (non-private accessibility) ◽️ Notice 4
Some values of the enum are not processed inside 'switch' statement ◽️ Notice 4
Use preferred style for trailing comma before new line in multiline lists ◽️ Notice 3
Auto-property can be made get-only (non-private accessibility) ◽️ Notice 3
RoslynAnalyzers The member referenced by the MemberData attribute returns untyped data rows ◽️ Notice 3
RoslynAnalyzers Do not use Enumerable methods on indexable collections ◽️ Notice 2
Property can be made init-only (non-private accessibility) ◽️ Notice 2
Auto-property can be made get-only (private accessibility) ◽️ Notice 1
RoslynAnalyzers Use concrete types when possible for improved performance ◽️ Notice 1
RoslynAnalyzers Use char overload ◽️ Notice 1
Dictionary lookup can be simplified with 'GetValueOrDefault' ◽️ Notice 1
'if-return' statement can be rewritten as 'return' statement ◽️ Notice 1
Foreach loop can be converted into LINQ-expression but another 'GetEnumerator' method will be used ◽️ Notice 1
Merge null/pattern checks into complex pattern ◽️ Notice 1
Redundant string interpolation ◽️ Notice 1
Replace with 'field' keyword ◽️ Notice 1
Some values of the enum are not processed inside 'switch' statement and are handled via default section ◽️ Notice 1
Tail recursive call can be replaced with loop ◽️ Notice 1
Type member is never accessed via base type (non-private accessibility) ◽️ Notice 1
Method return value is never used (non-private accessibility) ◽️ Notice 1
Literal length can be reduced by using verbatim string ◽️ Notice 1

☁️ View the detailed Qodana report

Detected 1 dependency

Third-party software list

This page lists the third-party software dependencies used in Loom

Dependency Version Licenses
Tomlyn 2.6.0 BSD-2-Clause
Contact Qodana team

Contact us at qodana-support@jetbrains.com

@R-unic
R-unic merged commit 351d78d into master Jul 24, 2026
3 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Loom Release 1.0.0 Jul 24, 2026
@R-unic
R-unic deleted the fix/infer-instantiated-function-types branch July 24, 2026 11:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Infer instantiated function types

1 participant