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

RFC0012 - Basic generics #115

Merged
merged 7 commits into from
Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
93 changes: 93 additions & 0 deletions Specification/Generics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Generics

_Note: Since we don't have user-defined types or traits yet, it makes little sense to define generics on user-defined types, or constraints on generic parameters. For now, this document will omit these._

## Generic functions

Generic functions are defined using the following syntax:

```swift
func foo<T1, T2, ...>() {
// ...
}
```

Where `T1`, `T2`, ... are the generic parameters, and can be referenced as types within the function signature and body. Example:

```swift
// ok -- -- ok
// v v
func foo<T1, T2, T3, T4>(x: T1): T2 {
var z: T3; // ok
}

var w: T4; // NOT OK, outside of signature and body
```

### Calling generic functions

Calling a generic function can be done with explicit instantiation:

```swift
foo<TypeArg1, TypeArg2, ...>();
```

Here, `TypeArg1`, `TypeArg2`, ... are type arguments.

If the type arguments can be inferred from context, the type argument can be partially, or even completely elided. Partial elision can be done by using the discard `_` syntax in the place of the elided type argument. Complete elision can be done by skipping the type argument list entirely.

Examples:

```cs
func second<T, U>(a: T, b: U): U = b;

// Explicitly instantiated
second<int32, string>(12, "hello");

// Partially elided, second argument is inferred
second<int32, _>(12, "hello");

// Partially elided, first argument is inferred
second<_, string>(12, "hello");

// Partially elided, both arguments are inferred
second<_, _>(12, "hello");

// Fully elided
second(12, "hello");
```

## Generic types

Generic types can be instantiated using the syntax `GenericType<TypeArg1, TypeArg2, ...>`, where `TypeArg1`, `TypeArg1`, ... are type parameters. For example, `List<T>` can be used with `int32` from `System.Collection.Generics` as `List<int32>`.

## Syntactical ambiguities

This proposed syntax is quite familiar for C# developers, but it does mean a slight syntactical ambiguity is introduced. The following syntactical construct become ambiguous:

```
name1 < name2 > (expr)
```

Where `name1` and `name2` are (potentially qualified) identifiers, and `expr` is an arbitrary expression.

The above could be interpreted in two different ways:
* A chained comparison between `name1`, `name2` and `expr`, where `expr` is simply a parenthesized expression.
* A generic function call of the function `name1` with the generic argument `name2` and call argument `expr`.

### Disambiguation

To syntactically disambiguate the two cases, a set of rules is defined to determine if a `<` starts a generic argument list, or is simply a comparison operator:
* If no matching `>` is found after the `<` within the expression, the sequence is deemed to be comparison
* If anywhere after `<` and before the matching `>` a syntactic construct is found that is only valid in expression context, the sequence is deemed to be comparison
* If anywhere after `<` and before the matching `>` a syntactic construct is found that is only valid in type context, the sequence is deemed to be a generic argument list (NOTE. that there is currently no such syntactic construct)
* If anywhere after `<` and before `>` a comma can be found between two top-level constructs, the sequence is deemed to be a generic argument list
* In any other case, the sequence stays ambiguous, and the follow-up token decides, how it is interpreted:
* A `(`, `.`, `,`, or any other punctuation character that can not continue a comparison expression right after a comparison operator deems the sequence a generic argument list
* In any other case, the sequence is considered to be a comparison

If the user wants to disambiguate to the other option than what would be inferred from these rules, they can do so by parenthesizing:
```swift
A<B>(C) // Generic call
(A)<B>(C) // Comparison chain
```
109 changes: 106 additions & 3 deletions Specification/Overloading.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Overloading

_Note: The current specification for overloading is temporary, because the compiler supports a very simple form of overloading. Once we have a more elaborate type-system specification, overloading will be expanded, but the properties described here will not be lost completely._
_Note: The current specification for overloading is missing a lot of details, like the scoring of subtyping. Once the language supports it, this document will be updated as well._

## Definition

Expand Down Expand Up @@ -58,9 +58,9 @@ func bar() {

## Resolution

Resolution gathers all visible overloads, and chooses the _single_ overload that has the exact parameter types, as the argument types on call-site. In case there is no such function, it is an overload resolution error.
Resolution gathers all visible overloads, and chooses the _single_ best matching overload. Best match is based on scoring the overloads.

Example:
Trivial examples that require no scoring system:

```swift
func foo(): string = "A";
Expand All @@ -75,3 +75,106 @@ func main() {
println(foo(1, "")); // D
}
```

### Scoring

When calling an overloaded function, each overload is assigned a score, which is a vector of numbers, representing how well each argument matches the given parameter signature.

* If any of the elements in the score vector is 0, the overload is discarded as non-matching.
* A vector of different dimensions is also discarded as non-matching (wrong number of arguments).
* An overload `A` is chosen over another, when the score vector of `A` dominates the one of `B`, meaning that each element in the vector `A` paired up with the ones in `B` greater or equal.
For example, `(2, 3, 4)` dominates `(2, 1, 0)`, as `2 >= 2`, `3 >= 1` and `4 >= 0`. There can be cases where neither vectors dominate each other: `(1, 2, 1)` and `(2, 1, 2)` for example.
* Two methods can dominate each-other by having the exact same overload scores.
* A final, unambiguous overload is only chosen, when there is an overload with a score that dominates all other scores, but is not dominated by any of the other scores.
* If by the end of resolution there are no non-discarded overloads remaining, the call causes a resolution error for no matching overloads found.
* If by the end of resolution there are multiple non-discarded overloads remaining, the call causes a resolution error for ambiguous overloading.

The following rules determine for the score for a single argument:
* Each argument starts with a unitary score (`1`).
* Type-incompatibility sets the score to `0`, discarding the overload.
* A generic parameter match halves the score.

Examples:

```swift
func identity<T>(x: T): T = x; // Overload 1
func identity(x: int32): int32 = x; // Overload 2

identity(true);
// # Overload 1
// - Overload 1 receives the score vector (1)
// - There is no type incompatibility
// - The argument matches a generic parameter, so the score is halved to (0.5)
//
// # Overload 2
// - Overload 2 receives the score vector (1)
// - The type bool is incompatible with int32, the score is set to (0)
// - There is a 0 element in the score vector, overload is discarded
//
// There is only a single overload remaining, Overload1, that one is chosen

identity(0);
// # Overload 1
// - Overload 1 receives the score vector (1)
// - There is no type incompatibility
// - The argument matches a generic parameter, so the score is halved to (0.5)
//
// # Overload 2
// - Overload 2 receives the score vector (1)
// - There is no type incompatibility, final score is (1)
//
// There are two score vectors remaining, Overload 1 with (0.5) and Overload 2 with (1). Since (1) dominates (0.5), Overload 2 is chosen.
```

```swift
func foo<T1, T2>(a: T1, b: int32, c: T2) {} // Overload 1
func foo<T>(a: int32, b: T, c: int32) {} // Overload 2

foo(true, 1, 2);
// # Overload 1
// - Overload 1 receives the score vector (1, 1, 1)
// - There is no type incompatibility
// - Since the first and last arguments match a generic parameter, their scores are halved, ending up with (0.5, 1, 0.5)
//
// # Overload 2
// - Overload 2 receives the score vector (1, 1, 1)
// - There is a type mismatch between int32 and bool, setting the score of the first component to 0, discarding this overload
//
// There is a single overload remaining, Overload 1 with a score of (0.5, 1, 0.5) is chosen.

foo(1, true, 2);
// # Overload 1
// - Overload 1 receives the score vector (1, 1, 1)
// - There is a type mismatch between int32 and bool, discarding this overload
//
// # Overload 2
// - Overload 2 receives the score vector (1, 1, 1)
// - There are no type mismatches
// - The second argument matches a generic type, halving its score (1, 0.5, 1)
//
// There is a single overload remaining, Overload 2 with a score of (1, 0.5, 1) is chosen.

foo(1, 2, 3);
// # Overload 1
// - Overload 1 receives the score vector (1, 1, 1)
// - There are no type mismatches
// - The first and the third arguments match a type parameter, so their scores are halved (0.5, 1, 0.5)
//
// # Overload 2
// - Overload 2 receives the score vector (1, 1, 1)
// - There are no type mismatches
// - The second argument matches a generic type, halving its score (1, 0.5, 1)
//
// We have Overload 1 with score (0.5, 1, 0.5) and Overload 2 with (1, 0.5, 1). Neither dominate each other, this is an ambiguous call.

foo(true, "hello", 3);
// # Overload 1
// - Overload 1 receives the score vector (1, 1, 1)
// - There is a type mismatch between string and int32, the overload is discarded
//
// # Overload 2
// - Overload 2 receives the score vector (1, 1, 1)
// - There is a type mismatch between bool and int32, the overload is discarded
//
// We have no remaining overloads, error for no matching overload.
```