dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.06k stars 1.55k forks source link

Incorrect num/int type inference in the analyzer #48257

Open lexaknyazev opened 2 years ago

lexaknyazev commented 2 years ago
Dart SDK version: 2.16.0-134.5.beta (beta) (Mon Jan 24 11:02:18 2022 +0100) on "macos_x64"
import 'dart:math';

void main() {
  final s = 1 + min(1, 0);
  print(List.filled(s, 0));
}

This code runs fine but the analyzer gives:

source.dart:5:21 • The argument type 'num' can't be assigned to the parameter type 'int'. • argument_type_not_assignable
lrhn commented 2 years ago

As I understand it, the analyzer is sadly correct.

The type of min is T Function<T extends num>(T, T).

The static typeof int.operator+ is num Function(num), with some special-cased rules for detecting that adding two integers gives an integer, and adding a double to anything gives a double. With no given context type, and no double values, those rules should not apply here.

That means that, final s = 1 + min(1, 0); calls min(1, 0) with a context type of num, which means that T gets instantiated to num. Then 1 + numValue has type num, and s get the static type num. Which is not assignable to int.

Am I missing something? @stereotype441

lexaknyazev commented 2 years ago

Couple more observations.

lrhn commented 2 years ago

Switching the order should fix the problem. The first operand of + is the receiver of the operator+ method call, and as such it gets no context type. Without a context type, type inference infers int as the type argument to min(1, 0) from the arguments. (With a context type, it always uses the context type as type argument.)

So, it's only the VM which fails to detect the type error. That's curious, since it should be using the same shared front-end as dart2js, which is what is used in DartPad.

stereotype441 commented 2 years ago

@lrhn

Am I missing something? @stereotype441

Yup, that analysis sounds correct to me.

So, it's only the VM which fails to detect the type error. That's curious, since it should be using the same shared front-end as dart2js, which is what is used in DartPad.

Yeah, that seemed curious to me too. I just tried reproducing the bug with version 2.16.0-134.5.beta (the version where the problem was reported), and for me, the VM gives me this error:

../../tmp/proj/test.dart:5:21: Error: The argument type 'num' can't be assigned to the parameter type 'int'.
  print(List.filled(s, 0));
                    ^
lexaknyazev commented 2 years ago

Found the reason of the VM's behavior. The file was in a folder with >=2.11.99 SDK config.

The issue could be reproduced on a standalone file by adding // @dart=2.9 to it.

stereotype441 commented 2 years ago

Found the reason of the VM's behavior. The file was in a folder with >=2.11.99 SDK config.

The issue could be reproduced on a standalone file by adding // @dart=2.9 to it.

Ah, ok. That makes sense. Dart version 2.9 was prior to the introduction of null safety. Back then, an expression of type num was permitted to be assigned to a variable of type int, and the type would be checked at runtime, in a manner similar to an as expression. So when you compile with // @dart=2.9, it's behaving as though you did this:

import 'dart:math';

void main() {
  final s = 1 + min(1, 0);
  print(List.filled(s as int, 0));
}

We removed this behavior when we introduced null safety for two reasons: 1. it was responsible for hiding a lot of bugs, and 2. these sorts of implicit type checks would have hidden the need for a lot of null checks (making the null safety feature a lot less valuable). I think it was a good change overall, but yeah, it does have the negative consequence of causing nuisance errors in situations like this one, where a runtime type check would have worked.

lexaknyazev commented 2 years ago

Thanks for the explanation.

The simplest way of bypassing the issue seems to be providing an explicit type argument for min like this:

import 'dart:math';

void main() {
  final s = 1 + min<int>(1, 0);
  print(List.filled(s, 0));
}

It works but, arguably, min<int>(1, 0) looks a bit too verbose. Maybe the analyzer or the linter could flag the + and similar operators when they convert their otherwise int operands to num.