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

CFE missing compile time error: const keys/switch cases should not implement == #32557

Closed
sigmundch opened this issue Mar 16, 2018 · 18 comments
Closed
Assignees
Labels
area-front-end Use area-front-end for front end / CFE / kernel format related issues. customer-dart2js front-end-fasta front-end-missing-error front-end-requires-constant-evaluation Issue that can't be solved without evaluating compile-time constants P2 A bug or feature request we're likely to work on
Milestone

Comments

@sigmundch
Copy link
Member

The spec doesn't allow const values that implement == to be used as keys in const maps, or as cases on switch statements.

The CFE is currently not producing a compile-time error for it.

Here is an example:

class A {
  final x;
  const A(this.x);
  operator==(y) => x < y.x;
}

const one = const A(1);
const two = const A(2);

const map = const {one: 1, two: 2};

main() {
  print(map[one]);
  print(map[two]);
  print(one == two);
}

Then:

> dart a.dart
'/a.dart': error: line 11 pos 20: key value must not implement operator ==
const map = const {one: 1, two: 2};
                   ^

> dart --preview-dart-2 a.dart
2
null
true

I wanted to file a bug for this issue because it accidentally hides a problem in dart2js, that Type cannot be used in these const contexts (see #17207), the sooner we handle this, the lower the chance we'll make a breaking change later on.

@sigmundch sigmundch added P2 A bug or feature request we're likely to work on front-end-fasta area-front-end Use area-front-end for front end / CFE / kernel format related issues. customer-dart2js labels Mar 16, 2018
@sigmundch sigmundch added this to the Dart2 Beta 3 milestone Mar 16, 2018
@lexaknyazev
Copy link
Contributor

While it's clear why an arbitrary class implementing operator == cannot be used as a compilation-time constant key, could TypeImpl be a special case? It uses a string _typeName which seems to be unique for each class (even when actual class names collide).

@dgrove
Copy link
Contributor

dgrove commented Jun 15, 2018

With -dev.59, the Dart Analyzer produces these errors:

Analyzing /usr/local/google/home/dgrove/tmp/dart2stable/32557.dart...
error • The constant map entry key expression type 'A' can't override the == operator at tmp/dart2stable/32557.dart:10:20 • const_map_key_expression_type_implements_equals
error • The constant map entry key expression type 'A' can't override the == operator at tmp/dart2stable/32557.dart:10:28 • const_map_key_expression_type_implements_equals
2 errors found.

@sigmundch
Copy link
Member Author

@askeksa-google - I believe this issue doesn't require constant evaluation, but only requires checking the type of the keys in a const map literal context.

@a-siva
Copy link
Contributor

a-siva commented Oct 11, 2018

The Dart VM does not produce any error on the above test.

@sigmundch
Copy link
Member Author

after some discussion at our dart2js meeting - I recognize that some const evaluation is needed, as a ternary expression yielding an Object as a LUB type would hide the issue.

@johnniwinther
Copy link
Member

Also if the switch case expression is a reference to a constant of static type dynamic, like const dynamic t = Object, do we need to evaluate the constant to catch all cases.

@lrhn
Copy link
Member

lrhn commented Oct 12, 2018

Since the requirement is for the class to not override Object.operator==, it should be sufficient to know the exact type of the constant value, without needing to know have the actual instance.

Conditions in constant expressions need to be completely evaluated to true or false in order to know which branch is used, so knowing the exact type may need some complete evaluation to happen.
Example:

const x = Bool.fromEnvironment("yadda") ? 1 : [1];
const m = {x: 42};

This program compiles and runs with -Dyadda=true, and it fails to compile without it.

@rakudrama
Copy link
Member

There is code in the wild that 'overrides' Object.operator == with an abstract method.

This is used as a mechanism for suppressing warnings when hashCode has an override but it is desired to inherit Object.operator ==.
It it intended to break this code? I don't see the code is harmful.

We we don't want to break this kind of code, care needs to be taken to allow constants that implement operator == via Object.operator== with a shadowing abstract method.

One example of this pattern is Protobuf enums: https://github.com/dart-lang/protobuf/blob/master/lib/src/protobuf/protobuf_enum.dart#L51

@lrhn
Copy link
Member

lrhn commented Oct 16, 2018

Abstract method declarations are not relevant to this - the requirement is that the class does not have an implementation of operator== different from the one inherited from Object. Abstract methods do not add an implementation, so we do precisely want to allow any class that implements operator== via Object.operator==.

The spec for switch says:

It is a compile-time error if the class C has an implementation of
the operator == other than the one inherited from Object,
unless the expression evaluates to a string or an integer,
or the expression is a literal symbol or
an invocation of a constant constructor of class Symbol.

For constant map literals, the key expressions must be

constant expressions evaluating to values that are either int or String instances, created using a symbol literal or a const invocation of the Symbol constructor, or instances of classes that that do not override the == operator inherited from Object,

(The switch one needs to be tweaked to match the map one, so that any expression evaluating to a constant symbol created by a symbol literal or a constant invocation of Symbol works, not only literal expression - which basically just means to allow const variables bound to such values).

The spec is woefully silent on the classes used to implement literals (values not created by calling a constructor). For the record, the intended semantics wrt. this for constant literal values are:

  • Null and bool do not override operator ==, neither do values created by list and map literals.
  • double and Type do override operator ==.
  • String, int and Symbol literals also override operator ==, but are allowed specially.
  • constant functions ... probably do not override operator ==. They are canonicalized as constants.

(We make no guarantees that map and list literals are all instances of the same class, though, which matters for switch. It's an annoying restriction).

@sigmundch
Copy link
Member Author

sigmundch commented Oct 18, 2018

Circling back to the original bug:

  • dart2js now has a workaround implemented that will check for operator== in switch cases.
  • we intend to delete it once the CFE implements the same feature, but we'll need a hook to add an exception to the rule.

In dart2js types are not canonicalized, so our TypeImpl implements operator==. However, type literals are constants and we do canonicalize them correctly, so most users will not encounter issues using type literals in switch cases.

Because all other tools do allow Type in switch cases, the dart2js-team is inclined to omit the error in the case of TypeImpl. Instead we'll treat this as a bug: types should be canonicalized in dart2js. We have plans to fix that, but until we do, we will need a way to suppress the error when the CFE implements it.

@lrhn
Copy link
Member

lrhn commented Oct 23, 2018

The issue with using type literals in switch cases is that you cannot use identical for comparison.
Example:

main() {
  var type = (<T>() => (<X>() => X)<List<T>>())<dynamic>();  // generates List<dynamic> dynamically.
  print(type == List ? "List<dynamic>" : "Not List<dynamic>");
  switch (type) {
    case List: print("List<dynamic>"); break;
    default: print("Not List<dynamic>");
  }
  print(const {List: "List<dynamic>"}[type] ?? "Not List<dynamic>");
}

If you run this code in dartpad (some version of dart2js), it runs and prints:

List<dynamic>
Not List<dynamic>
List<dynamic>

That's unlikely to be what the user intends.
(The same issue applies to symbols, a new Symbol("foo") does not match case #foo, but it is equal to it).

We explicitly did not want types to be canonicalized since there can be arbitrarily many different type objects in a program's life-time. Canonicalization would require you to remember those, which is a memory leak waiting to happen.

@vsmenon vsmenon modified the milestones: Dart2.1, Dart2.2 Oct 23, 2018
@sigmundch
Copy link
Member Author

@lrhn are you suggesting that switches on Type should always be a compile-time error? Or that switch statements should use ==?

dart-bot pushed a commit that referenced this issue Oct 24, 2018
Also mention it in the `external const factory Symbol` constructor.

This only relevant for constant expressions that may be used as
switch case expressions or constant map keys.

See #32557.

Bug: http://dartbug.com/32557
Change-Id: Ie82799f3f0d39c21c10765338a7dfeb74a582add
Reviewed-on: https://dart-review.googlesource.com/c/81242
Commit-Queue: Lasse R.H. Nielsen <lrn@google.com>
Reviewed-by: Leaf Petersen <leafp@google.com>
Reviewed-by: Erik Ernst <eernst@google.com>
@eernstg
Copy link
Member

eernstg commented Oct 24, 2018

Discussions are ongoing, it might be allowed. But the current specification prevents case clauses from using objects with a concrete == operator that overrides the one in Object, so it is currently an error (even for an identifier/typeName which is a constant expression of type Type). It seems to be an unenforced one, so we have usages out there.

PS: Switch statements are already specified to use == (search 'Matching of a case clause' in the spec), and this has been true at least since 59611b2, 2013-10-07.

@kmillikin
Copy link

This should be fixed by constant-update-2018, but I still see some test status file entries with this issue number so it needs verification. (Perhaps we do not catch the switch cases?)

@askeksa-google
Copy link

askeksa-google commented Apr 12, 2019

Yeah, the switch checks are not implemented. These fall in the category of "subsequent checks" which will likely require some Kernel format changes to support fully.

@kmillikin
Copy link

This depends on CFE constant evaluation and doesn't block it (constant evaluation).

@kmillikin kmillikin modified the milestones: D24 Release, Future May 22, 2019
@aadilmaan aadilmaan modified the milestones: Future, D25 Release Jun 4, 2019
@rakudrama rakudrama assigned johnniwinther and unassigned kmillikin Jun 19, 2020
@rakudrama
Copy link
Member

Some updates:

  • dart2js has a new Type implementation for which == is Object.==.
  • CFE seems to implement the error

@johnniwinther can you confirm that the CFE checks are complete?

@johnniwinther
Copy link
Member

These checks have all been implemented.

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. customer-dart2js front-end-fasta front-end-missing-error front-end-requires-constant-evaluation Issue that can't be solved without evaluating compile-time constants P2 A bug or feature request we're likely to work on
Projects
Dart2 Beta 3
  
To do
Development

No branches or pull requests