diff --git a/changes/stdlib-variance.feature.md b/changes/stdlib-variance.feature.md new file mode 100644 index 0000000000..a98a389589 --- /dev/null +++ b/changes/stdlib-variance.feature.md @@ -0,0 +1 @@ +- Iterator and generator types are covariant in their yield and return types and contravariant in their resume type, `Array.arrayLike` is covariant, and `taggedTemplate` is contravariant in its parameters and covariant in its output. diff --git a/compiler/ml/predef.ml b/compiler/ml/predef.ml index aca107a559..de6f65ee62 100644 --- a/compiler/ml/predef.ml +++ b/compiler/ml/predef.ml @@ -367,7 +367,8 @@ let common_initial_env add_type add_extension empty_env = decl_abstr with type_params = [tvar1; tvar2]; type_arity = 2; - type_variance = [Variance.full; Variance.full]; + type_variance = + [Variance.conjugate Variance.covariant; Variance.covariant]; } in diff --git a/packages/@rescript/runtime/Stdlib_Array.res b/packages/@rescript/runtime/Stdlib_Array.res index fc639b5a55..98f04f47f5 100644 --- a/packages/@rescript/runtime/Stdlib_Array.res +++ b/packages/@rescript/runtime/Stdlib_Array.res @@ -1,5 +1,5 @@ type t<'a> = array<'a> -type arrayLike<'a> +type arrayLike<+'a> @new external makeUninitializedUnsafe: int => array<'a> = "Array" @set external truncateToLengthUnsafe: (array<'a>, int) => unit = "length" diff --git a/packages/@rescript/runtime/Stdlib_Array.resi b/packages/@rescript/runtime/Stdlib_Array.resi index d411a10b54..32a06640eb 100644 --- a/packages/@rescript/runtime/Stdlib_Array.resi +++ b/packages/@rescript/runtime/Stdlib_Array.resi @@ -8,7 +8,7 @@ Type representing an array of value `'a`. */ type t<'a> = array<'a> -type arrayLike<'a> +type arrayLike<+'a> /** `asIterable(array)` views `array` as an `Iterable.t`. diff --git a/packages/@rescript/runtime/Stdlib_AsyncGenerator.res b/packages/@rescript/runtime/Stdlib_AsyncGenerator.res index 82b684b260..440ed4b4f2 100644 --- a/packages/@rescript/runtime/Stdlib_AsyncGenerator.res +++ b/packages/@rescript/runtime/Stdlib_AsyncGenerator.res @@ -1,4 +1,4 @@ -type t<'yield, 'return, 'next> = Stdlib_AsyncIterable.t<'yield> +type t<+'yield, +'return, -'next> = Stdlib_AsyncIterable.t<'yield> external asAsyncIterable: t<'yield, 'return, 'next> => Stdlib_AsyncIterable.t<'yield> = "%identity" external asAsyncIterableIterator: t<'yield, 'return, 'next> => Stdlib_AsyncIterableIterator.t< diff --git a/packages/@rescript/runtime/Stdlib_AsyncGenerator.resi b/packages/@rescript/runtime/Stdlib_AsyncGenerator.resi index a61859ad49..805f31253d 100644 --- a/packages/@rescript/runtime/Stdlib_AsyncGenerator.resi +++ b/packages/@rescript/runtime/Stdlib_AsyncGenerator.resi @@ -10,7 +10,7 @@ See [`AsyncGenerator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/R /** Type representing an async generator object. */ -type t<'yield, 'return, 'next> = private Stdlib_AsyncIterable.t<'yield> +type t<+'yield, +'return, -'next> = private Stdlib_AsyncIterable.t<'yield> /** `asAsyncIterable(generator)` views `generator` as an `AsyncIterable.t`. diff --git a/packages/@rescript/runtime/Stdlib_AsyncIterableIterator.res b/packages/@rescript/runtime/Stdlib_AsyncIterableIterator.res index 56ecbca2f3..3b36a2c0cf 100644 --- a/packages/@rescript/runtime/Stdlib_AsyncIterableIterator.res +++ b/packages/@rescript/runtime/Stdlib_AsyncIterableIterator.res @@ -1,4 +1,4 @@ -type t<'yield, 'return, 'next> = Stdlib_AsyncIterable.t<'yield> +type t<+'yield, +'return, -'next> = Stdlib_AsyncIterable.t<'yield> let make: (unit => promise>) => t< 'yield, diff --git a/packages/@rescript/runtime/Stdlib_AsyncIterableIterator.resi b/packages/@rescript/runtime/Stdlib_AsyncIterableIterator.resi index 0ee87335f4..a5d46ed667 100644 --- a/packages/@rescript/runtime/Stdlib_AsyncIterableIterator.resi +++ b/packages/@rescript/runtime/Stdlib_AsyncIterableIterator.resi @@ -10,7 +10,7 @@ See [`AsyncIterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Re /** Type representing an async iterable iterator. */ -type t<'yield, 'return, 'next> = private Stdlib_AsyncIterable.t<'yield> +type t<+'yield, +'return, -'next> = private Stdlib_AsyncIterable.t<'yield> /** `make(nextFn)` creates an async iterable iterator from a function returning the next result. diff --git a/packages/@rescript/runtime/Stdlib_AsyncIterator.res b/packages/@rescript/runtime/Stdlib_AsyncIterator.res index 1d2a4ea5d0..fb2e28f0e3 100644 --- a/packages/@rescript/runtime/Stdlib_AsyncIterator.res +++ b/packages/@rescript/runtime/Stdlib_AsyncIterator.res @@ -1,7 +1,7 @@ @notUndefined -type t<'yield, 'return, 'next> +type t<+'yield, +'return, -'next> -type rawResult<'yield, 'return> +type rawResult<+'yield, +'return> type result<'yield, 'return> = Stdlib_Iterator.result<'yield, 'return> diff --git a/packages/@rescript/runtime/Stdlib_AsyncIterator.resi b/packages/@rescript/runtime/Stdlib_AsyncIterator.resi index 760e2b0651..9f25e851a6 100644 --- a/packages/@rescript/runtime/Stdlib_AsyncIterator.resi +++ b/packages/@rescript/runtime/Stdlib_AsyncIterator.resi @@ -15,14 +15,14 @@ Type representing an async iterator protocol object. - `'next` is the type that can be sent back into `next(value)` */ @notUndefined -type t<'yield, 'return, 'next> +type t<+'yield, +'return, -'next> /** Result returned by `AsyncIterator.next`. */ type result<'yield, 'return> = Stdlib_Iterator.result<'yield, 'return> -type rawResult<'yield, 'return> +type rawResult<+'yield, +'return> /** Low-level raw JavaScript async iterator result shape. diff --git a/packages/@rescript/runtime/Stdlib_Generator.res b/packages/@rescript/runtime/Stdlib_Generator.res index a58e745e74..be7e615206 100644 --- a/packages/@rescript/runtime/Stdlib_Generator.res +++ b/packages/@rescript/runtime/Stdlib_Generator.res @@ -1,4 +1,4 @@ -type t<'yield, 'return, 'next> = Stdlib_Iterable.t<'yield> +type t<+'yield, +'return, -'next> = Stdlib_Iterable.t<'yield> external asIterator: t<'yield, 'return, 'next> => Stdlib_Iterator.t<'yield, 'return, 'next> = "%identity" diff --git a/packages/@rescript/runtime/Stdlib_Generator.resi b/packages/@rescript/runtime/Stdlib_Generator.resi index 54a1d93089..920df80bc3 100644 --- a/packages/@rescript/runtime/Stdlib_Generator.resi +++ b/packages/@rescript/runtime/Stdlib_Generator.resi @@ -10,7 +10,7 @@ See [`Generator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refere /** Type representing a generator object. */ -type t<'yield, 'return, 'next> = private Stdlib_Iterable.t<'yield> +type t<+'yield, +'return, -'next> = private Stdlib_Iterable.t<'yield> /** `asIterator(generator)` views `generator` as an `Iterator.t`. diff --git a/packages/@rescript/runtime/Stdlib_IterableIterator.res b/packages/@rescript/runtime/Stdlib_IterableIterator.res index fc6939510e..11a4d9c613 100644 --- a/packages/@rescript/runtime/Stdlib_IterableIterator.res +++ b/packages/@rescript/runtime/Stdlib_IterableIterator.res @@ -1,4 +1,4 @@ -type t<'yield, 'return, 'next> = Stdlib_Iterable.t<'yield> +type t<+'yield, +'return, -'next> = Stdlib_Iterable.t<'yield> let make: (unit => Stdlib_Iterator.result<'yield, 'return>) => t< 'yield, diff --git a/packages/@rescript/runtime/Stdlib_IterableIterator.resi b/packages/@rescript/runtime/Stdlib_IterableIterator.resi index d45566b0df..ff6b517740 100644 --- a/packages/@rescript/runtime/Stdlib_IterableIterator.resi +++ b/packages/@rescript/runtime/Stdlib_IterableIterator.resi @@ -14,7 +14,7 @@ See [Iteration protocols](https://developer.mozilla.org/en-US/docs/Web/JavaScrip /** Type representing an iterable iterator. */ -type t<'yield, 'return, 'next> = private Stdlib_Iterable.t<'yield> +type t<+'yield, +'return, -'next> = private Stdlib_Iterable.t<'yield> /** `make(nextFn)` creates an iterable iterator from a function returning the next result. diff --git a/packages/@rescript/runtime/Stdlib_Iterator.res b/packages/@rescript/runtime/Stdlib_Iterator.res index b160f94f4b..a8d7bceaf6 100644 --- a/packages/@rescript/runtime/Stdlib_Iterator.res +++ b/packages/@rescript/runtime/Stdlib_Iterator.res @@ -1,7 +1,7 @@ @notUndefined -type t<'yield, 'return, 'next> +type t<+'yield, +'return, -'next> -type rawResult<'yield, 'return> +type rawResult<+'yield, +'return> @tag("done") type result<'yield, 'return> = diff --git a/packages/@rescript/runtime/Stdlib_Iterator.resi b/packages/@rescript/runtime/Stdlib_Iterator.resi index ad22ec0d70..9c1178edd3 100644 --- a/packages/@rescript/runtime/Stdlib_Iterator.resi +++ b/packages/@rescript/runtime/Stdlib_Iterator.resi @@ -15,7 +15,7 @@ Type representing an iterator protocol object. - `'next` is the type that can be sent back into `next(value)` */ @notUndefined -type t<'yield, 'return, 'next> +type t<+'yield, +'return, -'next> /** Result returned by `Iterator.next`. @@ -25,7 +25,7 @@ type result<'yield, 'return> = | @as(false) Yield({value: 'yield}) | @as(true) Return({value: 'return}) -type rawResult<'yield, 'return> +type rawResult<+'yield, +'return> /** Low-level raw JavaScript iterator result shape. diff --git a/packages/@rescript/runtime/Stdlib_IteratorObject.res b/packages/@rescript/runtime/Stdlib_IteratorObject.res index b474d21a76..427a54a304 100644 --- a/packages/@rescript/runtime/Stdlib_IteratorObject.res +++ b/packages/@rescript/runtime/Stdlib_IteratorObject.res @@ -1,4 +1,4 @@ -type t<'yield, 'return, 'next> = Stdlib_Iterable.t<'yield> +type t<+'yield, +'return, -'next> = Stdlib_Iterable.t<'yield> type result<'yield, 'return> = Stdlib_Iterator.result<'yield, 'return> diff --git a/packages/@rescript/runtime/Stdlib_IteratorObject.resi b/packages/@rescript/runtime/Stdlib_IteratorObject.resi index 3764a4161c..2d3938bf78 100644 --- a/packages/@rescript/runtime/Stdlib_IteratorObject.resi +++ b/packages/@rescript/runtime/Stdlib_IteratorObject.resi @@ -12,7 +12,7 @@ See [`Iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Referen /** Type representing an `IteratorObject` instance. */ -type t<'yield, 'return, 'next> = private Stdlib_Iterable.t<'yield> +type t<+'yield, +'return, -'next> = private Stdlib_Iterable.t<'yield> /** Result returned by `IteratorObject.next`. diff --git a/tests/build_tests/super_errors/expected/GeneratorResumeTypeWidening.res.expected b/tests/build_tests/super_errors/expected/GeneratorResumeTypeWidening.res.expected new file mode 100644 index 0000000000..d45fd2b8c6 --- /dev/null +++ b/tests/build_tests/super_errors/expected/GeneratorResumeTypeWidening.res.expected @@ -0,0 +1,18 @@ + + We've found a bug for you! + /.../fixtures/GeneratorResumeTypeWidening.res:2:51-88 + + 1 │ // A generator resumed with `[#X]` can't be resumed with `[#X | #Y]`. + 2 │ let widen = (g: Generator.t) => (g :> Generator.t) + 3 │ + + Type Generator.t = Generator.t + is not a subtype of + Generator.t = Generator.t + + The second polymorphic variant is closed and doesn't include the constructor: #Y. + + Possible solutions: + - Either make the second variant open so it can accept additional constructors. To do this, make sure the type starts with [> instead of [ + - Or add the missing constructor to it. \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/GeneratorResumeTypeWidening.res b/tests/build_tests/super_errors/fixtures/GeneratorResumeTypeWidening.res new file mode 100644 index 0000000000..2b2ebd3c6e --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/GeneratorResumeTypeWidening.res @@ -0,0 +1,2 @@ +// A generator resumed with `[#X]` can't be resumed with `[#X | #Y]`. +let widen = (g: Generator.t) => (g :> Generator.t) diff --git a/tests/tests/src/stdlib_variance_test.mjs b/tests/tests/src/stdlib_variance_test.mjs new file mode 100644 index 0000000000..a9fb19cb68 --- /dev/null +++ b/tests/tests/src/stdlib_variance_test.mjs @@ -0,0 +1,107 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mocha from "mocha"; +import * as Test_utils from "./test_utils.mjs"; +import * as Stdlib_TaggedTemplate from "@rescript/runtime/lib/es6/Stdlib_TaggedTemplate.mjs"; + +function widenIterator(i) { + return i; +} + +function widenAsyncIterator(i) { + return i; +} + +function widenRawResult(r) { + return r; +} + +function widenAsyncRawResult(r) { + return r; +} + +function widenGenerator(g) { + return g; +} + +function widenAsyncGenerator(g) { + return g; +} + +function widenIterableIterator(i) { + return i; +} + +function widenAsyncIterableIterator(i) { + return i; +} + +function widenIteratorObject(i) { + return i; +} + +function widenArrayLike(a) { + return a; +} + +function widenTaggedTemplate(t) { + return t; +} + +function fail(param) { + return ((function* () {})()); +} + +function map(m, param) { + return m; +} + +let Effect = { + fail: fail, + map: map +}; + +let failing = fail("A"); + +let letters = (function* () { + yield "A" +}); + +let joinTag = Stdlib_TaggedTemplate.make((strings, params) => strings.join("|") + ":" + params.map(p => p).join(",")); + +Mocha.describe("Stdlib_variance_test", () => { + Mocha.test("a widened generator yields the same values", () => { + let widened = letters(); + let seen = []; + for (let x of widened) { + seen.push(x); + } + Test_utils.eq("File \"stdlib_variance_test.res\", line 64, characters 7-14", ["A"], seen); + }); + Mocha.test("a tag accepting more parameters is used where fewer are expected", () => Test_utils.eq("File \"stdlib_variance_test.res\", line 69, characters 7-14", "a|b:x", joinTag`a${"x"}b`)); +}); + +let useWithB = failing; + +let useWithC = failing; + +export { + widenIterator, + widenAsyncIterator, + widenRawResult, + widenAsyncRawResult, + widenGenerator, + widenAsyncGenerator, + widenIterableIterator, + widenAsyncIterableIterator, + widenIteratorObject, + widenArrayLike, + widenTaggedTemplate, + Effect, + failing, + useWithB, + useWithC, + letters, + joinTag, +} +/* failing Not a pure module */ diff --git a/tests/tests/src/stdlib_variance_test.res b/tests/tests/src/stdlib_variance_test.res new file mode 100644 index 0000000000..e93b8ef51c --- /dev/null +++ b/tests/tests/src/stdlib_variance_test.res @@ -0,0 +1,71 @@ +open Mocha +open Test_utils + +// Iterator-like types are covariant in their yield and return types and +// contravariant in their resume type, so these coercions type-check. +let widenIterator = (i: Iterator.t<[#A], [#R], [#X | #Y]>) => + (i :> Iterator.t<[#A | #B], [#R | #S], [#X]>) +let widenAsyncIterator = (i: AsyncIterator.t<[#A], [#R], [#X | #Y]>) => + (i :> AsyncIterator.t<[#A | #B], [#R | #S], [#X]>) +let widenRawResult = (r: Iterator.rawResult<[#A], [#R]>) => + (r :> Iterator.rawResult<[#A | #B], [#R | #S]>) +let widenAsyncRawResult = (r: AsyncIterator.rawResult<[#A], [#R]>) => + (r :> AsyncIterator.rawResult<[#A | #B], [#R | #S]>) +let widenGenerator = (g: Generator.t<[#A], [#R], [#X | #Y]>) => + (g :> Generator.t<[#A | #B], [#R | #S], [#X]>) +let widenAsyncGenerator = (g: AsyncGenerator.t<[#A], [#R], [#X | #Y]>) => + (g :> AsyncGenerator.t<[#A | #B], [#R | #S], [#X]>) +let widenIterableIterator = (i: IterableIterator.t<[#A], [#R], [#X | #Y]>) => + (i :> IterableIterator.t<[#A | #B], [#R | #S], [#X]>) +let widenAsyncIterableIterator = (i: AsyncIterableIterator.t<[#A], [#R], [#X | #Y]>) => + (i :> AsyncIterableIterator.t<[#A | #B], [#R | #S], [#X]>) +let widenIteratorObject = (i: IteratorObject.t<[#A], [#R], [#X | #Y]>) => + (i :> IteratorObject.t<[#A | #B], [#R | #S], [#X]>) +let widenArrayLike = (a: Array.arrayLike<[#A]>) => (a :> Array.arrayLike<[#A | #B]>) + +// A tag is contravariant in its parameters and covariant in its output. +let widenTaggedTemplate = (t: taggedTemplate<[#A | #B], [#R]>) => + (t :> taggedTemplate<[#A], [#R | #S]>) + +// A library type defined as an alias of `Generator.t` stays polymorphic under +// the relaxed value restriction, so one value can be used at several types. +module Effect: { + type step<+'e> + type t<'a, 'e> = Generator.t, 'a, unknown> + let fail: 'e => t<'a, 'e> + let map: (t<'a, 'e>, 'a => 'b) => t<'b, 'e> +} = { + type step<+'e> + type t<'a, 'e> = Generator.t, 'a, unknown> + let fail = _ => %raw(`(function* () {})()`) + let map = (m, _) => Obj.magic(m) +} + +let failing = Effect.fail(#A)->Effect.map(x => x + 1) +let useWithB: Effect.t = failing +let useWithC: Effect.t = failing + +let letters: unit => Generator.t<[#A], unit, unit> = %raw(`function* () { + yield "A" +}`) + +let joinTag: taggedTemplate<[#x | #y], string> = TaggedTemplate.make(( + strings, + params: array<[#x | #y]>, +) => strings->Array.join("|") ++ ":" ++ params->Array.map(p => (p :> string))->Array.join(",")) + +describe(__MODULE__, () => { + test("a widened generator yields the same values", () => { + let widened = (letters() :> Generator.t<[#A | #B], unit, unit>) + let seen = [] + for x of widened { + seen->Array.push(x) + } + eq(__LOC__, [#A], seen) + }) + + test("a tag accepting more parameters is used where fewer are expected", () => { + let onlyX = (joinTag :> taggedTemplate<[#x], string>) + eq(__LOC__, "a|b:x", onlyX`a${#x}b`) + }) +})