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

Inconsistent type demoting of a template #55021

Closed
seha-bot opened this issue Feb 26, 2024 · 5 comments
Closed

Inconsistent type demoting of a template #55021

seha-bot opened this issue Feb 26, 2024 · 5 comments
Labels
area-front-end Use area-front-end for front end / CFE / kernel format related issues.

Comments

@seha-bot
Copy link

Dart 3.3.0

Consider this snippet:

final class Foo<T> {
  const Foo(this.bar);
  final T bar;
}

List<Foo> get foos1 => const [Foo(1), Foo("2")];
List get foos2 => const [Foo(1), Foo("2")];

void main() {
  final List<Foo> wrong = foos1;
  print(wrong[0].runtimeType);
  
  final List<Foo> right = foos2.cast<Foo>().toList();
  print(right[0].runtimeType);

  final List alsoRight = foos2;
  print(alsoRight[0].runtimeType);
}

Output:

Foo<dynamic>
Foo<int>
Foo<int>

I don't think there is a valid reason for foos1's elements losing their type.

Second example:

final class Foo<T> {
  const Foo(this.bar);
  final T bar;
}

void main() {
  Foo<dynamic> a = Foo(1);
  print(a.runtimeType);
  print(a.bar.runtimeType);
  
  dynamic b = Foo(1);
  print(b.runtimeType);
  print(b.bar.runtimeType);
}

In the above example bar's type is never forgotten, but a is demoted to Foo<dynamic> even tho the instance is of type Foo<int>.

@kevmoo kevmoo added the area-front-end Use area-front-end for front end / CFE / kernel format related issues. label Feb 27, 2024
@kevmoo
Copy link
Member

kevmoo commented Feb 27, 2024

Starting with front-end here, although might be language

@johnniwinther
Copy link
Member

@chloestefantsova Can you take a look at this?

@chloestefantsova
Copy link
Contributor

It looks like a case of a specified behavior that somewhat goes against the intuitive expectations.

The static type of the foos1 is declared as List<Foo>. The type argument of Foo is omitted, which makes the type inference provide the default type for it. In this case it's dynamic. Then the type of foos1, that is, List<Foo<dynamic>>, is used as type context to infer its initializer, const [Foo(1), Foo("2")]. Later in the downwards inference phase, the type inference processes the elements of the list literal and extracts Foo<dynamic> from List<Foo<dynamic>> to serve as type context for them. Then, Foo(1) and Foo("2") are each inferred in the context Foo<dynamic>, resulting in dynamic being the type argument for both. Therefore, we get Foo<dynamic>(1) and Foo<dynamic>("2").

In the case of foos2, the type argument to List is omitted, and the default dynamic is provided. Then List<dynamic> is used as the type context for inferring const [Foo(1), Foo("2")]. Later in the downwards inference phase, the type inference processes the elements of the list literal and extracts dynamic from List<dynamic> to serve as the type context for them. Then, Foo(1) and Foo("2") are each inferred in the context dynamic, which doesn't suggest anything about their type arguments. Therefore, the upwards inference is used to infer the type arguments, resulting in Foo<int>(1) and Foo<String>("2"), and the type argument of the type literal being dynamic.

If my analysis is correct, this is closer to a language issue than a front-end issue. @eernstg, WDYT?

@eernstg
Copy link
Member

eernstg commented Feb 28, 2024

I'm not sure it's an issue, it is all working as intended! ;-)

As @chloestefantsova explained, the type arguments used during the creation of various instances of Foo is guided by the context type ("you get what you ask for"). This is a quite fundamental property of Dart type inference. It is usually a good choice to give the context type a high priority because actual type arguments in Dart are reified and Dart uses dynamically checked covariance with all type parameters (check out dart-lang/language#524 if you're interested in having some extra features in that area).

For example:

void main() {
  List<num> xs = [1];
  xs.add(1.5); // OK?
}

In this example, xs has the declared type List<num> because the developer intends to have a List<num> such that it is OK to add 1.5 to it later. That's exactly what we get because [1] is inferred with context type List<num>, and hence it becomes <num>[1].

However, we could also have said (in a hypothetical language which isn't quite Dart) "that list has its own type, we don't care what the context wants". We would then have inferred [1] as <int>[1] because every element in the list allows us to use the type argument int (and it's the "best" type that we can find which has that property). This is allowed (List<int> is assignable to List<num>), but if we do that then xs.add(1.5) will throw (because we're trying to add an element to a List<int> whose type isn't int).

So, in general, if your program specifies an expectation about the type of an expression then type inference will try to make it so.

By the way, [1] does become <int>[1] in the case where there is no context type, or the context type doesn't imply any preferences about the element type (e.g., the context type could be dynamic or Object rather than List<../*something*/..>).

Next, it's a well-known source of confusion that missing type arguments are provided implicitly by taking the bound (and using dynamic if there is no declared bound). So if you specify Foo as a type or as a type argument then it means Foo<dynamic>.

If you want to avoid having dynamic introduced implicitly as part of your types then you can choose to perform the static analysis of your programs in a slightly more strict manner by putting some or all of the following options into analysis_options.yaml:

analyzer:
  language:
    strict-raw-types: true
    strict-inference: true
    strict-casts: true

linter:
  rules:
    - avoid_dynamic_calls

In particular, strict-raw-types would give you a heads-up in situations where you are using a type like List<Foo>, where the type is processed to mean List<Foo<dynamic>>.

@eernstg
Copy link
Member

eernstg commented Feb 28, 2024

I'll close this issue because it doesn't report anything that is working in a way that is unintended. @seha-bot, you're of course welcome to create a new issue if needed, here or in the language repository.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-front-end Use area-front-end for front end / CFE / kernel format related issues.
Projects
None yet
Development

No branches or pull requests

5 participants