Skip to content

Analyzer issue with complex generic functions #33159

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

Closed
kevmoo opened this issue May 17, 2018 · 7 comments
Closed

Analyzer issue with complex generic functions #33159

kevmoo opened this issue May 17, 2018 · 7 comments
Assignees
Labels
legacy-area-analyzer Use area-devexp instead.
Milestone

Comments

@kevmoo
Copy link
Member

kevmoo commented May 17, 2018

Expected: all good. Runs fine, even w/ --preview-dart-2
Actual:

  error • The argument type '(<S₀>(String, (Object) → S) → S₀) → Config' can't be assigned to the parameter type '(<S>(String, (Object) → S) → S) → Config' at lib/_testHarness.dart:7:23 • argument_type_not_assignable
  error • The return type 'int' isn't a 'S', as defined by anonymous closure at lib/_testHarness.dart:7:67 • return_of_invalid_type_from_closure
class Config {
  final int value;
  Config(this.value);
}

Config fromJson(Map json) =>
    checkMethod(json, (check) => new Config(check('value', (v) => v as int)));

typedef _CheckedConvert = S Function<S>(String key, _CastFunction<S>);

typedef _CastFunction<R> = R Function(Object);

T checkMethod<T>(
  Map map,
  T constructor(_CheckedConvert converter),
) {
  Q _checkedConvert<Q>(String key, _CastFunction<Q> castFunc) {
    return castFunc(map[key]);
  }

  return constructor(_checkedConvert);
}

CC @leafpetersen – I'll let you clean-up the labels

@leafpetersen
Copy link
Member

Cut down example:

typedef _CheckedConvert = S Function<S>(String key, _CastFunction<S>);

typedef _CastFunction<R> = R Function(Object);

int Function(_CheckedConvert converter) f = (check) => 3;

@stereotype441
Copy link
Member

I'm able to reproduce this bug. Having a look now.

@kevmoo
Copy link
Member Author

kevmoo commented May 21, 2018 via email

@stereotype441
Copy link
Member

This has been an extremely tricky bug, due to two design assumptions of the analyzer that no longer hold true in Dart 2.0:

  • All function types are determined by starting with the type of an element in the element model, and substituting for free type variables in that type. In other words, function types never need to be created "out of thin air" based simply on parameter types and return types. Technically this wasn't even true in Dart 1.0 since LUB computation required creating types out thin air. In Dart 2.0, there are more instances of needing to create types out of thin air (such as the manipulations performed by StrongTypeSystemImpl._substituteForUnknownType), and due to type inference, the types created out of thin air get stored in the element model where they are long-lived.
  • The set of free type variables in a function type may be found by starting with associated element and following the chain of enclosingElement pointers, gathering up type parameters at each level of the element tree. This doesn't work because in many (perhaps all) cases where function types created out of thin air, it is difficult to find a good place in the element tree to attach the synthetic element to, so we just leave the synthetic element's enclosingElement pointer as null.

In Leaf's cut down example, the way the problem manifests is that type inference infers a type of <S>(String, (Object) → S) → S for the check parameter of the anonymous function, but this inference process requires creating the parameter type (Object) → S out of thin air. As a result, the type (Object) → S is disconnected from the element model, so when the analyzer later attempts to substitute other types for S, the parameter type (Object) → S is unaffected. This causes two follow-on problems:

  • When the type of check is printed, the analyzer does trial substitutions to determine whether the string representation of the type will contain ambiguous references to free type varialbles, and adds subscripts if necessary to disambiguate. These trial substitutions go wrong, causing the type to be incorrectly printed as <S₀>(String, (Object) → S) → S₀.
  • When the type of check is compared against the type of _CheckedConvert in order to determine if the assignment is valid, a fresh type variable is substituted for S on both sides of the comparison (in an effort to ensure that function types that differ only in the names of their type parameters will still compare equal). This substitution goes wrong, causing the analyzer to incorrectly conclude that the assignment is invalid.

It would be nice to rewrite FunctionTypeImpl to fix these assumptions, but that's a big job that will take time (in part because the assumptions are baked into the analyzer API). I'm currently trying to find a pragmatic way to get this bug fixed, and move FunctionTypeImpl a few steps toward a better implementation, without having to do a complete rewrite now.

@kevmoo
Copy link
Member Author

kevmoo commented Jun 8, 2018

@stereotype441
Copy link
Member

Fix is now ready for review

@leafpetersen
Copy link
Member

Woot! Great to see this bug well fixed. The old Dart 1 function type representation has been plaguing us. I'm betting there are a number of other open issues I've filed over the past year or two that have been closed by this as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
legacy-area-analyzer Use area-devexp instead.
Projects
None yet
Development

No branches or pull requests

3 participants