sdk icon indicating copy to clipboard operation
sdk copied to clipboard

Named parameter first weird issue/bug

Open busslina opened this issue 1 year ago • 12 comments
trafficstars

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...

busslina avatar Feb 16 '24 00:02 busslina

Item Details
Summary The order of named parameters in a method call matters.
Triage to area-language (high confidence)

(what's this?)

dart-github-bot avatar Feb 16 '24 00:02 dart-github-bot

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.

lrhn avatar Feb 16 '24 07:02 lrhn

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"

busslina avatar Feb 16 '24 07:02 busslina

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 avatar Feb 16 '24 09:02 eernstg

@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.

busslina avatar Feb 16 '24 11:02 busslina

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,
      );
}

busslina avatar Feb 16 '24 11:02 busslina

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 avatar Feb 16 '24 12:02 busslina

@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.

eernstg avatar Feb 16 '24 12:02 eernstg

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 avatar Feb 16 '24 13:02 chloestefantsova

@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

busslina avatar Feb 16 '24 14:02 busslina

Wait, I'm gonna check if all my private packages are on the correct git branch

busslina avatar Feb 16 '24 14:02 busslina

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

busslina avatar Feb 16 '24 14:02 busslina

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.

chloestefantsova avatar Feb 20 '24 07:02 chloestefantsova

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?

mraleph avatar Feb 20 '24 08:02 mraleph

My guess would be CFE inserts checks out of order?

Quite possible! Thanks for the repro, Slava! 👍

chloestefantsova avatar Feb 20 '24 08:02 chloestefantsova

Do you know on what Dart version will be applied the fix to test it?

busslina avatar Feb 21 '24 18:02 busslina

@busslina 3.4.0-162.0.dev or newer.

mraleph avatar Feb 22 '24 13:02 mraleph

@busslina 3.4.0-162.0.dev or newer.

Tx

busslina avatar Feb 22 '24 14:02 busslina