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

Feature: Statically checked declaration-site variance #524

Open
eernstg opened this issue Aug 14, 2019 · 96 comments
Open

Feature: Statically checked declaration-site variance #524

eernstg opened this issue Aug 14, 2019 · 96 comments
Labels
feature Proposed language feature that solves one or more problems variance Issues concerned with explicit variance

Comments

@eernstg
Copy link
Member

eernstg commented Aug 14, 2019

This is the tracking issue for introducing a statically checked mechanism for declaration-site variance in Dart.

Background: The original request motivating this kind of feature is dart-lang/sdk#213; the initial proposal for declaration-site invariance is dart-lang/sdk#213. The initial proposal for the related feature known as use-site invariance is dart-lang/sdk#229, and the corresponding tracking issue is dart-lang/sdk#753.

Note that this issue does come up in practice. Here are a few examples gathered since April 2023:

The text below describes properties of this feature which are good candidates for being adopted. Many things can still change, and a full feature specification will be written and used to manage the discussions about the final design.

Variance in Dart Today

As of Dart 2.4 or earlier, every type variable declared for a generic class is considered covariant. The core meaning of this is that a parameterized type C<T2> is a subtype of C<T1> whenever T2 is a subtype of T1. Other subtype rules can then be used to show subtype relationships like List<int> <: Iterable<dynamic> and Map<String, String> <: Map<Object, Object> <: dynamic.

This type rule is not sound; that is, in order to maintain heap soundness it is necessary to check certain types dynamically. This means that a program with no compile-time errors can fail with a type error at run time.

For instance, with the declaration List<num> xs and some expression e with static type num, it is necessary to check during evaluation of xs.add(e) that the value of e actually has the type which is required by xs: It is possible that it is a List<int> or even a List<Never>, and it would then be a dynamic type error if the value of e is a double, even though the expression had no type errors at compile-time.

Dynamically checked covariance enables many software designs that would be rejected by a traditional statically checked approach to variance (e.g., as in Java or C#). This allows developers to make a trade-off between more flexible types (e.g., a variable of type List<num> is allowed to refer to a List<int>) in return for accepting the potential dynamic type errors (a List<int> will work safely under the type List<num> in a lot of ways, just not all).

We want to enable a statically checked typing discipline for variance as well (rejecting more programs, but providing a compile-time guarantee against the run-time type errors described above). This feature is concerned with the provision of support for that.

Declaration-site Variance

Declaration-site variance can be used to declare a strict and statically checked treatment of variance for each type variable of a generic class.

Syntactically, declaration-site variance consists in allowing each type parameter declaration of a generic class declaration to include one of the following modifiers: out, in, or inout. We say that such a type parameter has explicit variance.

The use of type parameters with explicit variance in the body of the enclosing class is restricted. It is a compile-time error for a type variable marked out to occur in a non-covariant position in the signature of a member declaration; and for a type variable marked in to occur in a non-contravariant position. For example:

abstract class Good<out X, in Y, inout Z> {
  X get m1;
  void set m2(Y value);
  Z m3<U extends Z>(List<Z> zs);
}

class Bad<out X, in Y> {
  Y get m1; // Error.
  void set m2(X value); // Error.
  Y m3<U1 extends X, U2 extends Y>(List<X> xs); // Error.
}

Here are some core properties of declaration-site variance:

We obtain the following subtype relationships: Let C be a generic class with one type parameter X. Assume that S is a subtype of T. If X is marked out then C<S> <: C<T>; if X is marked in then C<T> <: C<S>. Note that there is no subtype relationship between C<S> and C<T> if X is marked inout, unless S == T.

A type parameter with explicit variance can be used in the specification of a superinterface. For example:

class A<out X, in Y, Z> {
  X get m;
}

class B<out X, inout Y, in Z> implements A<X, Y, Z> {}

Soundness is ensured via a number of rules like the following: It is a compile-time error if a type parameter X marked out occurs in a non-covariant position in an actual type argument for a superinterface D when the corresponding type parameter of D is marked out; and if X occurs in a non-contravariant position in an actual type argument for D when the corresponding type parameter is marked in; and if X occurs at all in an actual type argument for D when the corresponding type parameter is marked inout.

The interaction with dynamically checked covariant type parameters is similarly guarded: It is a compile-time error if a type parameter X marked out occurs in a non-covariant position in an actual type argument for a superinterface D when the corresponding type parameter of D has no explicit variance.

In return for all these restrictions, we get static safety: For a class where all type parameters have explicit variance, every (non-dynamic) member access which is statically type correct is also dynamically safe (no type checks on parameter types etc. are needed at run time).

In general, declaration-site variance can be used for classes which are intended to be strictly type checked with respect to variance everywhere.

@eernstg eernstg added the feature Proposed language feature that solves one or more problems label Aug 14, 2019
@eernstg eernstg added the variance Issues concerned with explicit variance label Aug 30, 2019
@eernstg
Copy link
Member Author

eernstg commented Sep 2, 2019

First draft of feature specification at PR dart-lang/sdk#557.

@lrhn
Copy link
Member

lrhn commented Sep 3, 2019

The "exact" type already exists in Dart in certain places - the type of a literal or object creation expression may have an exact type, which is why List<int> l = <num>[1]; is a compile-time error instead of a run-time downcast failure.
I assume those cases can be subsumed by exact types and still behave the same.

For use-site invariance, we will have both List<Foo> and List<exact Foo> as instantiations, with the latter being a subtype of the former. We don't actually introduce a nww type called exact Foo, just new syntactic forms for type arguments, and a suitably expanded type relation between instantiated generic types. We can say that we introduce exact Foo, but it's not a type, just a type pattern.

As stated, the type arguments can only be used in type pattern expressions (type annotations), not class expressions or literals.
That is, you can write List<exact Foo> x; as a type annotation, but not class C extends Iterable<exact int> or new List<exact int>(). Those make no sense, they are always "exact" in that they are run-time invocations with a single value.

The following should be allowed:

List<exact String> foo(List<exact int> list) => (list..add(42)).map((x) => "$x").toList();

It is meaningful and useful.

What about:

class C<T> {
  List<exact T> mkList() => <T>[];
}

Is this meaningful? Maybe. If you have a C<num> c, then you only know that the result of c.mkList is List<num>. If you have C<exact num> d; then the static type of d.mkList is (or should be) List<exact num>. If the code had not written exact T in the return type, then it would not have been true. The method:

  List<T> mkList() => <T>[];

will never have a static return type of List<exact anything>, not even for D<exact num>.mkList().

So, we need some substitution rules for type arguments to make this work.
exact T[exact S/T] -> exact S
exact T[S/T] -> S
T[exact S/T] -> S (?)

It also suggests that a lot of platform libraries should be changed to have exact in their return type, because they are really intended to be exact. Take: Set<T>'s Set<T> union(Set<T> other). Here we should make it Set<exact T> union(Set<T> other), so that if I have a Set<exact num> then the union returns another Set<exact num>.
(I just realized that I have written exact instead of exactly everywhere. I'm not particularly fond of all these keywords, but shorter seems better :)

That suggests a migration issue. If we change some platform libraries to return, say, List<exact String>, then any existing implementation of the same interface will no longer be valid when it returns merely List<String>, a super-type of the required return type. We may want to have an NNBD-like migration process where legacy libraries are accepted for a while, until everybody has migrated.

If we need migration anyway, then we don't need to make the syntax backwards compatible, and we could do something else, like:

class C<+T , -S, =U> {  // out T, in S, inout U
   List<+T> mkList() => <T>[]; // not exact T
   List<U> mkList() => <U>[]; // exact U
}

We do get the inherent complexity of a two-tier type system where it matters whether you write exact int or int on every type argument in your API. You have to get it right, otherwise you make lock yourself out of possible later changes.
If you say foo(List<exact num> arg) then you can't add elements to arg later, and you prevent anyone with merely a List<num> from calling you. They'll have to do list.cast<exactly num>() to convert their list.

I expect exact types in arguments to be rare (you really do need to do modification), and for them to be common in return types.

For declarations site variance, we already have something similar for function types. For those, the variance is automatically computable from the co-/contra-variance of the occurrences of the type arguments.
We could do the same for class parameters, but that would break all our existing unsafely covariant classes. We could do it anyway, and require migration which would effectively mean that all existing type arguments become covariant, and we write an explicit covariant on all existing contravariant occurrences (well, the ones in parameters, we can't help the ones in function return types).

@eernstg
Copy link
Member Author

eernstg commented Sep 3, 2019

The "exact" type already exists

Right. The notion of an exact type is not specified, but it is used in some cases by our tools (even to raise an error for certain "impossible" casts).

The notion of a type argument which is known exactly is a different concept (an object can have dynamic type List<exactly T> even though it is an instance of some proper subtype of List). I just checked the text above to make sure that it doesn't ignore that distinction, and adjusted a couple of sentences.

For use-site invariance, we will have both List<Foo> and List<exact Foo> as instantiations

Both of those are instantiations of the generic type List (where instantiation of a generic type means providing actual type arguments), but we will not have an object whose dynamic type is List<Foo>. If the dynamic type T of a given object o is such that List<Foo> is one of the superinterfaces of T then o has type List<exactly Foo>. In general, every type argument in the dynamic type of an object is exactly something, and the superinterfaces will carry this property with them.

class A<X, Y> {}
class B<X> implements A<X, int> {}

With that, B<String>() has dynamic type B<exactly String>, and it is also of type A<exactly String, exactly int>.

We may or may not want to let Type.toString() reveal this bit, but it must be present in the dynamic representation of types in order to maintain soundness.

we introduce exact Foo, but it's not a type, just a type pattern

I'd prefer to say that a type accepts type arguments, each of which may have the modifier exactly, which also implies that exactly Foo is not a type.

the type arguments can only be used in type pattern expressions (type annotations),
not class expressions or literals.

exactly can be used on type arguments, e.g., <List<exactly num>>[], but not on types, and we need to make the distinction that "type arguments" given prescriptively are types. So List<exactly num> is OK as a type annotation, but List<exactly num>() as an instance creation is not, and <exactly num>[] is not; but when exactly is nested one level deeper then it is again a type argument, which is the reason why <List<exactly num>>[] is OK.

class C<T> {
  List<exactly T> mkList() => <T>[];
}

That can be allowed, and would be safe, but the computation of the member access type must take into account that the statically known value of T is just an upper bound.

main() {
  C<num> c = C<int>();
  List<num> xs = c.mkList(); // Static type of `c.mkList()` is `List<num>`.
  List<exactly num> ys = c.mkList(); // Error (downcast).
}

Of course, with class C<inout T> we would have an uninterrupted chain of exactness, and c.mkList() would have static type List<exactly num>.

we need some substitution rules

I believe the most straightforward way to get this right is to consider the types of member accesses, with a case for each variance of the type parameters of the class. Type parameters with no variance modifier are the most complex ones, of course, because they are allowed to occur in any position in a member signature.

We may want to have an NNBD-like migration process where legacy libraries
are accepted for a while

Indeed.

we don't need to make the syntax backwards compatible

Right, but C<=X, -Y> x; may not be optimally readable (and in, out, inout isn't all that verbose). In any case, that's probably not more breaking than the keywords.

do get the inherent complexity of a two-tier type system where it matters
whether you write exact int or int on every type argument in your API

I think that wouldn't ideally be such a common situation: The declaration site variance modifiers are required to match the usage (so if you have a type parameter out E then it just can't be the type annotation of a method parameter), and a subtype receiver will have a subtype member (including: less specific parameter types and more specific return types).

The use of exactly in a member signature would be specifically concerned enhancing the type safety in the management of legacy types (with type parameters that have no variance modifiers).

I would expect the reasonable trade-off to be somewhere between just using the legacy types as they are (with the current level of type safety, which has been used in practice for years without complete panic) and teasing out the very last drop of static type safety, by adding exactly as many places as possible. In any case, there are rules for eliminating exactly from interface type members such that the resulting typing is sound.

@He-Pin
Copy link

He-Pin commented Dec 20, 2019

I more like the +T and -T syntax in Scala

@He-Pin
Copy link

He-Pin commented Dec 23, 2019

The current in out looks like a primary student and verbose.
Kotlin and C# is using in and out too,but reads badly.

@leafpetersen
Copy link
Member

Is it the AUTHOR of the Bad class, or the USER of said class? Sure, user will benefit, too, if the author makes fewer mistakes, but other than that?

The primary beneficiary is the USER. The List class is one of the primary examples of something that should be invariant, but is instead unsoundly covariant. We have numerous users who are frustrated that this results in unexpected runtime errors that could have been caught at compile time. Enclosed below is an innocuous looking test program that fails at runtime instead of at compile time. Sound variance allows users who strongly prefer to rule these errors out at compile time to do so.

void addToList<T>(List<T> target, List<T> src) {
  for(var x in src) {
    target.add(x);
  }
}

void test(List<num> nums) {
  addToList(nums, [3, 4, 5]);
}
void main() {
  List<double> doubles = [3.5];
  test(doubles);
}

@leafpetersen
Copy link
Member

Question 2:  you seem to be leaning towards the use of "inout" as a synonym of "exact", but I have difficulty understanding the reasoning leading from "inout" to "exact".

Reading between the lines of this question, I think you are missing the primary effect of this feature, which is that it changes the subtyping relation between types. Describing inout as exact is sensible because given class Invariant<inout X> ... we know that any variable of type Invariant<num> contains only objects that were allocated as Invariant<num> or a direct subclass thereof. Specifically, it will never contain an Invariant<int>, nor an Invariant<Object>. In this sense then, it contains objects whose generic parameters are exactly as described by the type. Hence exact. Does that help?

@mockturtl
Copy link

mockturtl commented Dec 23, 2019

Describing inout as exact is sensible

I think tatumizer is suggesting exact will make a better (easier to understand) syntax for invariance. (Compare: "let xy mean neither x, nor y.")

@leafpetersen
Copy link
Member

I find the word inout confusing. It's difficult to see how this word may carry the meaning of "exact".

Ah, sorry, I misunderstood your question. Here's the intution:

  • a type variable labelled in may only be used to pass things in to methods
    • void add (T x)
  • a type variable labelled out may only be used to pass things out of methods
    • T get first
  • a type variable labelled inout may be used to pass things in to and out of methods
    • T swap(T x)

Does that help?

(By the way, what combination of "in" and "out" characterizes the current default behavior in dart?)

Neither. Classes are allowed to use type variables everywhere in a method, which corresponds most closely to inout, but subtyping is covariant, which corresponds to out.

Let's consider your example with addToList. What will our new program look like to prevent this bad outcome? Where "in", "out" and "inout" should be added? And what line in the code will be flagged statically after we do these changes?

For completeness, here is the example rewritten to use an example of an invariant implementation of List. The call to test on the second line of main is statically rejected.

import 'dart:collection';

class Array<inout T> extends ListBase {
  List<T> _store;
  int get length => _store.length;
  void set length(int i) => _store.length = i;
  T operator[](int index) => _store[index];
  void operator[]=(int index, T value) => _store[index] = value;
  Array.fromList(List<T> this._store);
}

void addToList<T>(Array<T> target, Array<T> src) {
  for(var x in src) {
    target.add(x);
  }
}

void test(Array<num> nums) {
  Array<num> ints = Array.fromList([3, 4, 5]);
  addToList(nums, ints);
}
void main() {
  Array<double> doubles = Array.fromList([3.5]);
  test(doubles);
}

@leafpetersen
Copy link
Member

@tatumizer I think this is getting a bit far afield - variance is definitely a confusing subject, but I don't think we're going to fix that here. Is it fair to summarize your general take here as being that you find + - = more intuitive than out in inout? Or are you expressing a preference for something different than that?

@He-Pin
Copy link

He-Pin commented Dec 24, 2019

repeat the inout and inout here and there make me looks like a fool; why not make use of inputParameter T outputParameterR?

@He-Pin
Copy link

He-Pin commented Dec 24, 2019

@leafpetersen I think the - and + style are more concise and the in and out style are more easy to understand.

@SandroMaglione
Copy link

Any updates on the status of this feature for release?

@mateusfccp
Copy link
Contributor

@SandroMaglione

I don't think the team has any ETA, but the feature is behind an experimental flag (--enable-experiment=variance).

It doesn't seems to be something the team is prioritizing, tho.

@plammens
Copy link

plammens commented Aug 1, 2024

First draft of feature specification at PR dart-lang/sdk#557.

I think the link should be #557 (dart-lang/language).

Same for the other links in the issue description.

@plammens
Copy link

plammens commented Aug 7, 2024

I don't think the team has any ETA, but the feature is behind an experimental flag (--enable-experiment=variance).

Unfortunately the experiment is also unusable... The following basic snippet fails:

// dummy class with contravariant type parameter
class Consumer<in T> {
  void consume(T item) {}
}

final List<Consumer<String>> consumers = [];
Consumer<String> consumer = Consumer<Object>();  // OK, both static analysis and runtime
consumer.consume("hello");  // OK, both static analysis and runtime
print(consumer is Consumer<String>);   // static analysis says this is always true, runtime prints false
consumers.add(consumer);  // runtime error: "Consumer<Object> is not a Consumer<String>" (but it is)

@iapicca
Copy link

iapicca commented Aug 7, 2024

Consumer<String> consumer = Consumer<Object>();  // OK, both static analysis and runtime

this feels more like a bug than a new feature

@plammens
Copy link

plammens commented Aug 7, 2024

Consumer<String> consumer = Consumer<Object>();  // OK, both static analysis and runtime

this feels more like a bug than a new feature

The bug is the current behaviour:

// no variance annotation, defaults to covariant
class Consumer<T> {
  void consume(T item) {}
}

Consumer<Object> consumer = Consumer<String>();  // OK, both static analysis and runtime
consumer.consume(123);  // static analysis ok, fails at runtime: we passed an int to a method which expects a String

It might feel strange because our intuition tells us that generics are covariant, and in some cases this is correct (if the type is an "output" type, as a rule of thumb). But some generics really should be contravariant (if the type is used for input only) or invariant (if the type is used for both input and output).

For instance, a consumer that accepts strings is not a consumer of objects, because it doesn't accept objects that are not strings. Conversely, a consumer that accepts any kind of object is certainly a consumer of strings, because you can pass it a string and it will deal with it no problem.

Unfortunately, Dart not only lacks variance annotation but defaults to covariant parameters, which leads to type unsoundess like the above example. A better default in terms of type safety would have been invariant.

@junaid1460
Copy link

Why these have be keywords? Why not go with typescript way? It allows much more ways to expand. Like for example doing things like ReturnType. Maybe doing something like All objects except num and string.

@eernstg
Copy link
Member Author

eernstg commented Nov 11, 2024

Late to the party, I must have overlooked these comments. Here we go:

@plammens wrote:

I don't think the team has any ETA, but the feature is behind an experimental flag (--enable-experiment=variance).

Unfortunately the experiment is also unusable...

True, the experiment is front-end only. The backends do not implement the subtyping relation for variance modifiers, which means that the backends will always assume that every class/mixin/enum type parameter is dynamically checked covariant (which is true as long as we do not have this feature). So the fact that this particular type parameter has the modifier in is ignored at run time, and we get the corresponding behavior.

The following basic snippet fails:

// dummy class with contravariant type parameter
class Consumer<in T> {
  void consume(T item) {}
}

final List<Consumer<String>> consumers = [];
Consumer<String> consumer = Consumer<Object>();  // OK, both static analysis and runtime
consumer.consume("hello");  // OK, both static analysis and runtime
print(consumer is Consumer<String>);   // static analysis says this is always true, runtime prints false
consumers.add(consumer);  // runtime error: "Consumer<Object> is not a Consumer<String>" (but it is)

The static checking accepts the initialization of consumer, and no run-time type check is performed at the initialization because static analysis has already established a guarantee that it is safe.

The situation at run time is now type correct, including the fact that a reference of type Consumer<String> refers to an instance of type Consumer<Object>. However, according to the backends the situation is a soundness violation, because they think it is untrue that Consumer<Object> is a subtype of Consumer<String>. So we can now have run-time failures at positions where no dynamic type check should be able to fail. (They are wrong run time failures, of course, because the backends have this upside-down understanding of the subtype relationship.)

consumer.consume("hello") is accepted at compile-time because a String can be passed to a parameter whose static type is known to be String or a supertype thereof. It is in fact Object, so it succeeds at run time.

The type test consumer is Consumer<String> is statically expected to be true, so we get the 'Info' that this is a useless expression. It would indeed be useless with a full implementation (that is, including backends) of the in modifier. However, the backends will ignore the in modifier and evaluate this test to false.

Finally, consumers.add(consumer) causes a run-time type error because the backends think that Consumer<Object> isn't a subtype of Consumer<String>.

So, indeed, the experiment can only be used to try out the static analysis, you won't (yet) get the correct semantics at run time.

However, if you're willing to use invariance rather than contravariance then you can emulate the variance modifier inout today. The point is that you use an extra type argument to maintain the invariance requirement (also known as a phantom type), and you use a type alias declaration to ensure that every client (in any other library, at least) must pass the intended value of that extra type argument:

/*
Wanted:

class Consumer<inout T> {
  void consume(T item) {}
}
*/
  
typedef Inv<X> = X Function(X);

typedef Consumer<T> = _Consumer<T, Inv<T>>;

class _Consumer<T, Invariance extends Inv<T>> {
  void consume(T item) {}
}

void main() {
  final List<Consumer<String>> consumers = [];
  // Consumer<String> consumer = Consumer<Object>(); // Error, not a subtype.
  Consumer<String> consumer = Consumer<String>(); // OK.

  consumer.consume("hello"); // OK.
  print(consumer is Consumer<String>); // Flagged as trivial. No harm, though.
  consumers.add(consumer); // OK.

  try {
    consumers.add(Consumer<Object>() as dynamic); // Throws.
  } catch (_) {
    print('Did throw on `Consumer<Object>`.');
  }
  try {
    consumers.add(Consumer<Never>() as dynamic); // Throws.
  } catch (_) {
    print('Did throw on `Consumer<Never>`.');
  }
}

The fact that the attempt to add a Consumer<Object> and a Consumer<Never> to consumers both fail illustrates that this emulation includes backend support: Neither covariant nor contravariant subtyping is available.

@eernstg
Copy link
Member Author

eernstg commented Nov 11, 2024

@plammens wrote:

A better default in terms of type safety would have been invariant.

True, invariance is the traditional approach, and it provides the guarantee that the run-time value of a type parameter is the same as the statically known value, which means that there will not be any failing run-time type checks.

However, you probably can't find a recent language with subtyping whose generics do not include some kind of enhancement in this area. Plain invariant generics is awfully rigid.

For example, Java introduced wildcarded types in Java 5 in 2004, and Scala had declaration-site variance at the same time. C# introduced in and out modifiers for interfaces and delegates in 2010.

Dart chose a different approach, based on the experiences with those explicitly specified variance related features in other languages. In particular, there was a lot of feedback in this area, claiming that variance related typing was too complex.

So Dart adopted an approach which is used with arrays in several other languages: All type parameters are considered covariant, and every situation where the typing isn't guaranteed statically are checked at run time. (In Java and C# this is the ArrayStoreException/ArrayTypeMismatchException, and in Dart it's a TypeError that occurs in situations like (<int>[1] as List<num>).add(1.5)).

I wasn't part of the Dart team at the time, and I was somewhat worried about this approach when I joined in 2014 because it isn't fully statically checked. However, I must admit that we have had amazingly few reports about difficulties created by those dynamic type checks. I've gathered 20 examples, not thousands (shown in the initial posting). Also, of course, soundness of the heap is strictly maintained because those run-time type checks are generated in every situation where the typing isn't guaranteed statically.

In summary, it would indeed have been extremely easy to just specify that Dart subtyping with generics is invariant. However, dynamically checked covariance was chosen because it is simple, and it actually works almost all the time in practice (in particular, all scenarios that are purely read-only with respect to the given type parameter are safe).

That said, I created this issue because I really want to give developers the option to specify a strict and fully statically checked approach to variance. This is especially relevant in the case where a functional-inspired style is used, and first class functions are used heavily.

In any case, the way ahead is to get this feature fully implemented. If we get more votes for it, it gets more likely.

copybara-service bot pushed a commit to dart-lang/sdk that referenced this issue Nov 15, 2024
This CL implements a new lint, `unsafe_variance`. This lint emits a
warning whenever an instance member declaration has a signature where
a type variable declared by the enclosing class/mixin/enum occurs in
a non-covariant position in the return type (including the type of
an instance variable).

Issues: https://github.com/dart-lang/linter/issues/4111, with goals related to dart-lang/language#296 and dart-lang/language#524.

Change-Id: I1352d71d61fece03a432ccf0d98825a69e3a457f
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/384700
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Erik Ernst <eernst@google.com>
@tolotrasamuel
Copy link

The experimental flag --enable-experiment=variance no longer works on Flutter Web as of Dart 3.5.4

dart-lang/sdk#57112

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems variance Issues concerned with explicit variance
Projects
Status: Being spec'ed
Development

No branches or pull requests