sdk
sdk copied to clipboard
Named parameter first weird issue/bug
I have a method with this signature:
static Future<bool> registerVisit(
llib.VisitData data, {
required bool reconnectionMode,
})
When calling it, this is working:
final decodedData = jsonDecode(message.encodedData);
VisitController.registerVisit(
llib.VisitData.parse(decodedData['timeZoneOffset'], ip),
reconnectionMode: decodedData['reconnectionMode'],
).then((res) {
respond(res, messageId: message.id);
});
But this is giving a runtime error:
final decodedData = jsonDecode(message.encodedData);
VisitController.registerVisit(
reconnectionMode: decodedData['reconnectionMode'],
llib.VisitData.parse(decodedData['timeZoneOffset'], ip),
).then((res) {
respond(res, messageId: message.id);
});
Error:
WebsocketConnectionV2.startListening() -- Error processing message -- type '_$VisitDataImpl' is not a subtype of type 'bool' -- WebsocketMessageV2(event: register-visit, encodedData: String)
Seems very strange...
| Item | Details |
|---|---|
| Summary | The order of named parameters in a method call matters. |
| Triage to | area-language (high confidence) |
How are you compiling this?
The behavior should be the same for the two pieces of code, as long as VisitData.parse doesn't modify decodedData.
The compiler which shows the difference is doing something wrong.
as long as VisitData.parse doesn't modify decodedData
decodedData['timeZoneOffset'] is a String value, so no possibility of VisitData.parse modifying decodedData:
factory VisitData.parse(String localDatetimeOffset, String ip) => VisitData(
localDatetimeOffset: DurationConverter().fromJson(localDatetimeOffset),
ip: ip,
);
I'm not compiling directly, I'm executing dart run.
As Dart has no dart doctor I share the dart --version output:
Dart SDK version: 3.2.4 (stable) (Thu Dec 21 19:13:53 2023 +0000) on "linux_x64"
You could try to separate out the semantic difference that we should have between those two snippets of code: The evaluation order of the actual arguments passed to the VisitData.parse factory:
... // Lots of stuff that we don't know.
void main() {
final VisitData visitData;
final bool reconnectionMode;
if (Random().nextBool()) {
visitData = llib.VisitData.parse(decodedData['timeZoneOffset'], ip);
reconnectionMode = decodedData['reconnectionMode'];
} else {
reconnectionMode = decodedData['reconnectionMode'];
visitData = llib.VisitData.parse(decodedData['timeZoneOffset'], ip);
}
VisitController.registerVisit(
visitData,
reconnectionMode: reconnectionMode,
).then((res) {
respond(res, messageId: message.id);
});
}
If true yields a working program and false causes a run-time error then it is indeed the evaluation order that makes the difference. Otherwise it could be a bug in the implementation of the named arguments everywhere feature. If it is the latter then it would be great if you could create a small, complete example that shows the buggy behavior.
@eernstg
This isolated test seems to work fine:
void main() {
_optionA();
_optionB();
}
void _optionA() {
print('optionA');
_registerVisitMock(
llib.VisitData.parse('1h', '192.168.1.1'),
reconnectionMode: false,
);
}
void _optionB() {
print('optionB');
_registerVisitMock(
reconnectionMode: false,
llib.VisitData.parse('1h', '192.168.1.1'),
);
}
void _registerVisitMock(
llib.VisitData data, {
required bool reconnectionMode,
}) =>
print('data: $data, reconnectionMode: $reconnectionMode');
Output:
optionA
data: VisitData(localDatetimeOffset: 1:00:00.000000, ip: 192.168.1.1), reconnectionMode: false
optionB
data: VisitData(localDatetimeOffset: 1:00:00.000000, ip: 192.168.1.1), reconnectionMode: false
Now I'm gonna test in the real project in order to get what's going on.
I thought maybe I did some changes to my freezed class without running the build runner but I ran it and no change happened.
Again, the weird bug comes in.
Option A:
case llib.Ws2.registerVisit:
final decodedData = jsonDecode(message.encodedData);
debug('decodedData: $decodedData');
// VisitController.registerVisit(
// llib.VisitData.parse(decodedData['timeZoneOffset'], ip),
// reconnectionMode: decodedData['reconnectionMode'],
// ).then((res) {
// respond(res, messageId: message.id);
// });
final data = llib.VisitData.parse(decodedData['timeZoneOffset'], ip);
final reconnectionMode = decodedData['reconnectionMode'];
print('data: $data');
print('reconnectionMode: $reconnectionMode');
VisitController.registerVisit(
data,
reconnectionMode: reconnectionMode,
).then((res) {
respond(res, messageId: message.id);
});
return llib.CascadeResult.breakk;
Output A:
Feb 16 11:46:19 server1.busslina.com dart[111845]: [I] @2 -- 16/02/2024 11:46:19 (UTC) -- 91.126.109.108 -- $1 -- register-visit
Feb 16 11:46:19 server1.busslina.com dart[111845]: [DEBUG] -- decodedData: {reconnectionMode: false, timeZoneOffset: 1h}
Feb 16 11:46:19 server1.busslina.com dart[111845]: data: VisitData(localDatetimeOffset: 1:00:00.000000, ip: 91.126.109.108)
Feb 16 11:46:19 server1.busslina.com dart[111845]: reconnectionMode: false
Feb 16 11:46:19 server1.busslina.com dart[111845]: [I] @3 -- 16/02/2024 11:46:19 (UTC) -- 91.126.109.108 -- $1 -- get-voting-info
Option B:
case llib.Ws2.registerVisit:
final decodedData = jsonDecode(message.encodedData);
debug('decodedData: $decodedData');
// VisitController.registerVisit(
// llib.VisitData.parse(decodedData['timeZoneOffset'], ip),
// reconnectionMode: decodedData['reconnectionMode'],
// ).then((res) {
// respond(res, messageId: message.id);
// });
final data = llib.VisitData.parse(decodedData['timeZoneOffset'], ip);
final reconnectionMode = decodedData['reconnectionMode'];
print('data: $data');
print('reconnectionMode: $reconnectionMode');
VisitController.registerVisit(
reconnectionMode: reconnectionMode,
data,
).then((res) {
respond(res, messageId: message.id);
});
return llib.CascadeResult.breakk;
Output B:
Feb 16 11:47:34 server1.busslina.com dart[111995]: [I] @2 -- 16/02/2024 11:47:34 (UTC) -- 91.126.109.108 -- $1 -- register-visit
Feb 16 11:47:34 server1.busslina.com dart[111995]: [DEBUG] -- decodedData: {reconnectionMode: false, timeZoneOffset: 1h}
Feb 16 11:47:34 server1.busslina.com dart[111995]: data: VisitData(localDatetimeOffset: 1:00:00.000000, ip: 91.126.109.108)
Feb 16 11:47:34 server1.busslina.com dart[111995]: reconnectionMode: false
Feb 16 11:47:34 server1.busslina.com dart[111995]: [E] WebsocketConnectionV2.startListening() -- Error processing message -- type '_$VisitDataImpl' is not a subtype of type 'bool' -- WebsocketMessageV2(event: register-visit, encodedData: String)
Feb 16 11:47:34 server1.busslina.com dart[111995]: [I] @3 -- 16/02/2024 11:47:34 (UTC) -- 91.126.109.108 -- $1 -- get-voting-info
I didn't do exactly what you suggested me @eernstg , but I will do it if this isn't enough.
part 'visit_data.freezed.dart';
part 'visit_data.g.dart';
@freezed
class VisitData with _$VisitData, SerializableWebSafe {
const VisitData._();
factory VisitData({
@DurationConverter() required Duration localDatetimeOffset,
required String ip,
}) = _VisitData;
factory VisitData.fromJson(JsonMap data) => _$VisitDataFromJson(data);
factory VisitData.parse(String localDatetimeOffset, String ip) => VisitData(
localDatetimeOffset: DurationConverter().fromJson(localDatetimeOffset),
ip: ip,
);
}
I added too a first print line to VisitController.registerVisit.
Output A:
Feb 16 12:04:41 server1.busslina.com dart[112284]: data: VisitData(localDatetimeOffset: 1:00:00.000000, ip: 91.126.109.108)
Feb 16 12:04:41 server1.busslina.com dart[112284]: reconnectionMode: false
Feb 16 12:04:41 server1.busslina.com dart[112284]: registerVisit() -- first line -- data: VisitData(localDatetimeOffset: 1:00:00.000000, ip: 91.126.109.108), reconnectionMode: false
Feb 16 12:04:41 server1.busslina.com dart[112284]: [I] @3 -- 16/02/2024 12:04:41 (UTC) -- 91.126.109.108 -- $1 -- get-voting-info
Output B:
Feb 16 12:05:44 server1.busslina.com dart[112435]: data: VisitData(localDatetimeOffset: 1:00:00.000000, ip: 91.126.109.108)
Feb 16 12:05:44 server1.busslina.com dart[112435]: reconnectionMode: false
Feb 16 12:05:44 server1.busslina.com dart[112435]: [E] WebsocketConnectionV2.startListening() -- Error processing message -- type '_$VisitDataImpl' is not a subtype of type 'bool' -- WebsocketMessageV2(event: register-visit, encodedData: String)
Feb 16 12:05:44 server1.busslina.com dart[112435]: [I] @3 -- 16/02/2024 12:05:44 (UTC) -- 91.126.109.108 -- $1 -- get-voting-info
That line doesn't execute on OptionB so it indicates the problem is with the function signature/arguments order.
class VisitController {
static Future<bool> registerVisit(
llib.VisitData data, {
required bool reconnectionMode,
}) async {
print(
'registerVisit() -- first line -- data: $data, reconnectionMode: $reconnectionMode');
[...]
@busslina, thanks for the additional info!
This really looks like a bug in the code generation: Apparently, in case B, the actual argument which is passed positionally (data) gets passed to the formal parameter reconnectionMode, The other actual argument might be passed to the formal parameter data or it might be ignored, etc., but that doesn't matter much: The main point is that the named parameter is bound to the positional actual argument, and that's a bug.
@chloestefantsova, @johnniwinther, it would be great if one of you could take a look at the scenario above: Is the "named arguments everywhere" feature processed entirely by the front end? In that case this looks like a CFE bug.
Thanks for reporting the bug, @busslina! I think @eernstg means the generation of the internal code representation inside of the compiler.
Looking at the code, I don't see anything that we shouldn't be able to handle. I've written a simplified isolated version of the program to demonstrate how we handle lifting the named arguments that are mentioned before positional
class A {
static B foo(String x, {required bool y}) => new B();
}
class B {
void bar(void Function() f) {}
}
test() {
A.foo(y: false, "").bar(() { print("first"); });
A.foo("", y: false).bar(() { print("second"); });
}
This program is translated into the following intermediate representation.
library;
import self as self;
import "dart:core" as core;
class A extends core::Object {
synthetic constructor •() → self::A
: super core::Object::•()
;
static method foo(core::String x, {required core::bool y}) → self::B
return new self::B::•();
}
class B extends core::Object {
synthetic constructor •() → self::B
: super core::Object::•()
;
method bar(() → void f) → void {}
}
static method test() → dynamic {
(let final core::bool #t1 = false in self::A::foo("", y: #t1)).{self::B::bar}(() → void {
core::print("first");
}){(() → void) → void};
self::A::foo("", y: false).{self::B::bar}(() → void {
core::print("second");
}){(() → void) → void};
}
As you can see, both lines in the body of the test function are translated into semantically equivalent code.
There may be something that we missing though: the code can be transformed later on, and that transformation can interfere with lifting of the named arguments. We might not handle the interaction between those two processes.
@busslina, are the formal parameters of VisitController.registerVisit annotated in any way or are subject to code transformation?
@chloestefantsova
@busslina, are the formal parameters of VisitController.registerVisit annotated in any way or are subject to code transformation?
No way. I only use code generation for freezed/json_serializable.
I tried to reproduce the issue with another test with the exact same function type/signature, but I'm unable to make it fail:
void main() {
_optionA();
_optionB();
}
class _Controller {
static Future<bool> registerVisitMock(
llib.VisitData data, {
required bool reconnectionMode,
}) async {
print('data: $data, reconnectionMode: $reconnectionMode');
return true;
}
}
void _optionA() {
print('optionA');
_Controller.registerVisitMock(
llib.VisitData.parse('1h', '192.168.1.1'),
reconnectionMode: false,
).then((value) {
print('A -- done');
});
}
void _optionB() {
print('optionB');
_Controller.registerVisitMock(
reconnectionMode: false,
llib.VisitData.parse('1h', '192.168.1.1'),
).then((value) {
print('B -- done');
});
}
Output:
optionA
data: VisitData(localDatetimeOffset: 1:00:00.000000, ip: 192.168.1.1), reconnectionMode: false
optionB
data: VisitData(localDatetimeOffset: 1:00:00.000000, ip: 192.168.1.1), reconnectionMode: false
A -- done
B -- done
Wait, I'm gonna check if all my private packages are on the correct git branch
Wait, I'm gonna check if all my private packages are on the correct git branch
Checked, everything was fine. Same bug. I recorded a video of it:
https://mega.nz/file/tO41GQ6K#PCV1ynpBYOEFg06Z-pSR5wOFbH62OSk_jpUtAGD4iTo
Thank you for the additional data, @busslina! We currently are trying to reproduce an issue and identify the conditions that cause the exception you're observing.
Reproduction:
void foo(String x, {required bool y}) {
}
void main() {
final v = <String, dynamic>{'y': true, 'x': ''};
foo(y: v['y'], v['x']);
}
I reproduced this by making an observation that original code is dealing with JSON which has dynamically typed values so CFE would need to insert implicit as checks. My guess would be CFE inserts checks out of order?
My guess would be CFE inserts checks out of order?
Quite possible! Thanks for the repro, Slava! 👍
Do you know on what Dart version will be applied the fix to test it?
@busslina 3.4.0-162.0.dev or newer.