Skip to content

Add eager inference annotation for polymorphic types #106

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

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Changes from 26 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
213ec9f
Create monomorphic-type-bindings-for-generics.md
Ukendio Mar 5, 2025
cb82f7a
Update monomorphic-type-bindings-for-generics.md
Ukendio Mar 5, 2025
bf354a1
Update and rename monomorphic-type-bindings-for-generics.md to eager-…
Ukendio Mar 5, 2025
8f1c0d9
Update "Function-parameter-bindings" alternative
hardlyardi Mar 5, 2025
8a24f5b
Add .md extension
hardlyardi Mar 5, 2025
9df20a0
Update eager-inference-annotations-for-polymorphic-types.md
Ukendio Mar 5, 2025
e25643f
change per-function-parameter to per-argument
hardlyardi Mar 6, 2025
1943be7
change "generic" to "polymorphic type" in a few places
hardlyardi Mar 7, 2025
1d36401
Punctuation Fixes
hardlyardi Mar 7, 2025
1c8d217
change "inference behavior" to "instantiation behavior" in alternative
hardlyardi Mar 7, 2025
2ed0187
Update eager-inference-annotations-for-polymorphic-types.md
hardlyardi Mar 7, 2025
b7c2800
add line break
hardlyardi Mar 7, 2025
b47e000
Update wording in summary
hardlyardi Mar 7, 2025
29384af
add comma
hardlyardi Mar 7, 2025
f5c5609
Update eager-inference-annotations-for-polymorphic-types.md
Ukendio Mar 7, 2025
b995140
remove redundant "union behavior"
hardlyardi Mar 7, 2025
5308d81
Update eager-inference-annotations-for-polymorphic-types.md
Ukendio Mar 7, 2025
c9328c7
Update eager-inference-annotations-for-polymorphic-types.md
Ukendio Mar 7, 2025
27d8c66
changes to alternatives
hardlyardi Mar 7, 2025
cbc02bd
general changes + rephrasing
hardlyardi Mar 7, 2025
9fa24be
add single char monospace
hardlyardi Mar 7, 2025
96f8aaa
Update eager-inference-annotations-for-polymorphic-types.md
Ukendio Mar 7, 2025
b83c473
small consistency/grammar change
hardlyardi Mar 7, 2025
1fee95b
add return statement
hardlyardi Mar 7, 2025
458dcb6
fix spelling :(
hardlyardi Mar 7, 2025
8bb92ba
Amend per-function-bindings to per-usage-bindings
hardlyardi Mar 7, 2025
7d20d52
separate noinfer alternative for clarity
hardlyardi Mar 7, 2025
2463447
Update eager-inference-annotations-for-polymorphic-types.md
hardlyardi Mar 7, 2025
6264249
Update eager-inference-annotations-for-polymorphic-types.md
Ukendio Mar 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions docs/eager-inference-annotations-for-polymorphic-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Eager Inference Annotations for Polymorphic Types

## Summary

The RFC introduces a feature to annotate polymorphic function types to express that the first instantiation of a polymorphic type `T` is the one that sticks.

## Motivation

The purpose of this feature is to develop syntax to prevent polymorphic types from widening into (e.g., number | string) when a function is implicitly instantiated with different argument types. E.g., `test(1, "a")`. In the following code, Luau's current solver infers `T` to be of a union type:
Copy link
Contributor

Choose a reason for hiding this comment

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

What is "Luau's current solver?"

We have two products right now, Luau's Type Solver and Luau's New Type Solver. "type solver" is really a brand name coined by @andyfriesen, not something external folks necessarily recognize. Could also reasonably describe it as Luau's V2 type inference engine or something of that sort, honestly not entirely sure how we want to deal with the language around the two type inference systems in RFCs.

Choose a reason for hiding this comment

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

You're right. I considered this while editing the motivation paragraph, but was unsure if "type inference engine V2" was even the correct terminology to use. For now I'll switch the terminology there to "Luau's Type Inference Engine V2"?


```luau
function test<T>(a: T, b: T): T
return a
end

local result = test(1, "string") -- inferred type `T`: number | string"
```

This behaviour can be useful in some cases but is undesirable when a polymorphic function is intended to constrain the input types to be consistent.
Copy link
Contributor

Choose a reason for hiding this comment

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

when a polymorphic function is intended to constrain the input types to be consistent.

This doesn't mean anything to me. The input type that was inferred in the example is already consistent. Every single instance of the generic T is indeed instantiated with the same consistent type, always, regardless of this RFC. That's just the semantics of polymorphism. I think the way you probably want to talk about your desire is something like... wanting to retain the exact type of the argument.

Choose a reason for hiding this comment

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

Oops. I'll change it to "constrain the subsequent input types to be identical to the first usage." or something, couldn't think of anything better at the time.


## Design

We propose adding some symbol as a suffix (or prefix) that annotates the "eager" inference behaviour for a polymorphic type.
Subsequent usages of type `T` where `T` is "eager" would be ignored during instantiation.
Copy link
Contributor

Choose a reason for hiding this comment

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

This description is misleading. They're not ignored during instantiation, you'll still replace each occurrence with the inferred type. The only thing you're proposing changes here, afaik, is that an annotated generic will opt call sites into inferring the exact type of the first argument for automatic instantiation.

Choose a reason for hiding this comment

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

You're absolutely right, "ignored" is the wrong term. I'll update it to "Subsequent uses of a polymorphic type T where T is "eager" will be inferred as the precise type of the first occurrence."


### New Syntax

The `!` syntax modifier would would enforce an eager inference behaviour for `T!`:

```luau
function test<T!>(a: T, b: T): T
return a
end

test(1, "string") -- TypeError: Expected `number`, got `string`
```

## Drawbacks

- Introduces a new syntax modifier `!`, which may lead to a symbol soup. However, `T!` doesn't seem too shabby next to `T?`.
Copy link
Contributor

Choose a reason for hiding this comment

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

T! doesn't seem too shabby next to T?

I think this is actually a reasonably strong argument against this syntax: people might confuse it as having something to do with optionals or nil, and it does not at all.

Choose a reason for hiding this comment

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

Copy link
Author

Choose a reason for hiding this comment

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

Yeah might lean onto using keywords or putting the annotation as a prefix instead !T.

- Introduces a simple change to luau's parser, marginally increasing parsing complexity.

## Alternatives
### Per-usage-bindings
Flip the relationship being declarared per-type-parameter to per-usage which provides more control in expressing the inference, and could allow both instantiation behaviours of polymorphic types under a uniform syntax.

A polymorphic typed marked with type `T!` will not contribute to the instantiation of type `T` in the function. Instead, `T` should be inferred on the arguments without the annotation:

```luau
function test<T>(first: T, second: T, third: T!): T
return first
end

test(1, "string", true) -- TypeError: Type `boolean` could not be converted into `number | string`
```

Notably, this behavior would be identical to other languages, such as typescript's `noinfer<T>`. This has the added drawback that the `!` syntax modifier would need to be barred from return types, as the return type holds no relevance to implicit instantiation.
Copy link

@Fireboltofdeath Fireboltofdeath Mar 7, 2025

Choose a reason for hiding this comment

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

I feel like a noinfer<T> type function should be listed more clearly as an alternative to the ! syntax, since it doesn't introduce any new keywords or symbols.

The current phrasing doesn't mention it as an alternative

Choose a reason for hiding this comment

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

I agree, I'll add an update shortly to add noinfer clearly as an individual alternative.



### Keywords
Something like `<greedy T>` or `<strict T>` should also be considered if we want to reduce symbols. This idea has merit when considering the potential complexity of type aliases combined with `T!?`
Copy link
Contributor

Choose a reason for hiding this comment

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

T!?

This is also a very strong argument against this syntax.

Copy link
Contributor

Choose a reason for hiding this comment

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

Though, is this actually possible under your proposal? You made ! a property of the definition at the generic, and you can't write optional in that context.

Choose a reason for hiding this comment

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

You're right, T!? wouldn't be possible under the main proposal. It is however possible under the per-usage-binding alternative with unique syntax, I'll move it to a drawback of that alternative where it should be.

Copy link
Author

@Ukendio Ukendio Mar 8, 2025

Choose a reason for hiding this comment

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

You're right, T!? wouldn't be possible under the main proposal. It is however possible under the per-usage-binding alternative with unique syntax, I'll move it to a drawback of that alternative where it should be.

The problem is with inference not syntax as the annotation is defined at the definition, not the argument so there is no conflict in syntax. The problem would be that if a binding is required to be inferred by the first argument that binds to T annotated that it is an optional then the inference simply wouldn't work unless we changed the behaviour to infer the first non optional argument that binds to T to dictate the type.

I am curious to know how it worked before with eager inference by default, as that is likely what we want to mirror.