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

Add eager inference annotation for polymorphic types #106

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Changes from all 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
72 changes: 72 additions & 0 deletions docs/eager-inference-annotations-for-polymorphic-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Eager Inference Annotations for Polymorphic Types

## Summary

The RFC introduces a feature to annotate polymorphic function types to express that the first bind to `T` will be the one that sticks.

Choose a reason for hiding this comment

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

What would be the behavior when a type references T more than once, such as an object?

function F<T!>(value: { a: T, b: T })

Additionally, what about when the object type is aliased (i.e there's no specific "first occurence")

type MyObject<T> = { a: T, b: T };

function F<T!>(value: MyObject<T>)

Copy link
Author

Choose a reason for hiding this comment

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

What would be the behavior when a type references T more than once, such as an object?

It would behave in the same way as the old type solver which is taking the first definition that binds to T.

Additionally, what about when the object type is aliased (i.e there's no specific "first occurence")

Similar answer to the last point which is mirroring the old behaviour but in this case would be to go in the order of the fields. I don't know the exact heuristics for that is but I feel most of this could be answered with just saying it should do whatever the type solver did.

I will note that I do think something like noinfer<T> is a pretty good solution in these cases but I haven't personally prepared to make a case for that proposal.


## 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 Type Inference Engine V2 infers `T` to be of a union type:

```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 type is intended to constrain the subsequent input types to be identical to the first usage.

## Design

We propose adding some symbol as a suffix (or prefix) that annotates the "eager" inference behaviour for a polymorphic type.
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`
```

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.
Also has the major drawback of symbol complexity. E.g., type aliases with `T!?` are entirely possible under this model.

### noinfer\<T\>
Same as above, except we introduce no new syntax, symbol usage, etc. into the language. Create a binding similar or equivalent to typescript's noinfer<T>:

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

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

### Keywords
Something like `<greedy T>` or `<strict T>` should also be considered if we want to reduce symbols.