sdk
sdk copied to clipboard
Error handling for expression evaluation throws with the wrong class
Repro:
main.dart:
import 'ext.dart' as ext;
void main() {
final i = 0;
print(i.foo());
}
ext.dart
extension E on int {
int foo() => this;
}
Place a breakpoint under main.dart at any line and add to watch:
i.foo()
The error shown in the expression evaluation is:
The method 'foo' isn't defined for the class '_Smi'.
Even when the class for i is an int.
Related: https://github.com/dart-lang/sdk/issues/56554
CC: @DanTup
Summary: Expression evaluation in the debugger incorrectly throws an error when evaluating an extension method call on an int value. The error message indicates that the method is not defined for the _Smi class, even though the value is an int.
//cc @derekxu16
My point in making two separate issues was so that this one focuses on the error message and the other one focuses on solving the error. But if you believe that they should be handled together thats alright by me.
_Smi is technically the type it has. While it might be unfortunate in this case because _Smi might not be something one is used to I generally see it as a good thing that the expression compilation knows about the actual types.
@jensjoha out of interest, what is _Smi and what's the relationship to int? If I evaluate i.runtimeType I get back something that includes int but doesn't mention Smi:
{
"jsonrpc": "2.0",
"result": {
"type": "@Instance",
"_vmType": "Type",
"class": {
"type": "@Class",
"fixedId": true,
"id": "classes/48",
"name": "_Type",
"_vmName": "_Type@0150898",
"location": {
"type": "SourceLocation",
"script": {
"type": "@Script",
"fixedId": true,
"id": "libraries/@0150898/scripts/dart%3Acore-patch%2Ftype_patch.dart/0",
"uri": "dart:core-patch/type_patch.dart",
"_kind": "kernel"
},
"tokenPos": 817,
"endTokenPos": 1112,
"line": 24,
"column": 1
},
"library": {
"type": "@Library",
"fixedId": true,
"id": "libraries/@0150898",
"name": "dart.core",
"uri": "dart:core"
}
},
"identityHashCode": 3678101524,
"kind": "Type",
"fixedId": true,
"id": "classes/184/types/0",
"typeClass": {
"type": "@Class",
"fixedId": true,
"id": "classes/184",
"name": "int",
"location": {
"type": "SourceLocation",
"script": {
"type": "@Script",
"fixedId": true,
"id": "libraries/@0150898/scripts/dart%3Acore%2Fint.dart/0",
"uri": "dart:core/int.dart",
"_kind": "kernel"
},
"tokenPos": 1265,
"endTokenPos": 16970,
"line": 28,
"column": 1
},
"library": {
"type": "@Library",
"fixedId": true,
"id": "libraries/@0150898",
"name": "dart.core",
"uri": "dart:core"
}
},
"name": "int"
},
"id": "22"
}
However, if I just evaluate i, it does return an instance with a class of _Smi. It's not obvious to me why those things are different (and I do think it's unfortunate that this message uses the one the user might have no knowledge of because it's an internal class).
We're getting into very technical territory here I think.
I'm not a VM engineer so this attempt at a short explanation might be wrong --- but here goes: In Dart everything is an object, but someone at some point found that having ints not actually be objects when they live in memory is a good thing for at least memory consumption (but probably also, if nothing else because of less memory consumption, for speed). To do this a tagging scheme is used where the least-significant bit is used for the tag: 0 is a Smi (by shifting the int up) and (pointers to) objects have a 1 (which I think is made a 0 when actually addressing stuff which is safe to do because of alignment). Because we shift the int up, though, we suddenly have less bits for storing the number. So Ints that use all bits can't be Smis but are Mints instead ("medium ints" I think --- once upon a time Dart had infinite precision ints which was probably called something else) - which are then actual objects.
From the outside it should still look like an object though (because everything is an object) so it's a _Smi. Why .runtimeType still says int I don't know --- but we can all overwrite what it says though, e.g.
void main() {
int i = 42;
print("i is ${i.runtimeType}");
Map<String, String> m = {};
print("m is ${m.runtimeType}");
Foo foo = new Foo();
print("foo is ${foo.runtimeType}");
foo.myType.name = "int";
print("but now foo is ${foo.runtimeType}");
foo.myType.name = "String";
print("and now foo is ${foo.runtimeType}");
}
class Foo {
MyType myType = new MyType();
Type get runtimeType => myType;
}
class MyType implements Type {
String name = "not set yet";
toString() => name;
}
will print
i is int
m is _Map<String, String>
foo is not set yet
but now foo is int
and now foo is String
which leads me to my next point: int vs _Smi might be weird but creating a Map but actually getting a _Map is sort of the same thing.
Additionally If you have an Object o but can see it's some other type (or it's a Foo but you can see it's a actually a FooImpl) you'd probably want to be able to run expression compilations on it as if it was that type. At least I do.
Anyway, _Smi is defined here:
https://github.com/dart-lang/sdk/blob/c73677e606f78b15efa25971699af1fdc5824fd6/sdk/lib/_internal/vm/lib/integers.dart#L535
with https://github.com/dart-lang/sdk/blob/c73677e606f78b15efa25971699af1fdc5824fd6/sdk/lib/_internal/vm/lib/integers.dart#L8
where on top of Smi we also have Mint: https://github.com/dart-lang/sdk/blob/c73677e606f78b15efa25971699af1fdc5824fd6/sdk/lib/_internal/vm/lib/integers.dart#L738
(with the dartdoc comment "Represents integers that cannot be represented by Smi but fit into 64bits." which at least doesn't contradict my explanation above)
@jensjoha thanks, that's really useful! I've seen something like a "OneByteString"(?) class show up in some of these places too, and I guess there's some similar optimisation going on there.
I think I agree, if a class like _Smi might be overriding runtimeType I think seeing the real type makes most sense - it's just a shame these names aren't something a user is more used to (I suspect if it had been called _SmallInteger or something, it would've been slightly less confusing/cryptic).