language icon indicating copy to clipboard operation
language copied to clipboard

Should a successful match promote the thing being matched?

Open stereotype441 opened this issue 3 years ago • 4 comments

@scheglov pointed out that it would be nice if this worked:

double calculateArea(Shape shape) {
  return switch (shape) {
    case Square() => shape.length * shape.length;
    case Circle() => math.pi * shape.radius * shape.radius;
  }
}

@munificent and I agree. Filing this issue as a reminder to think about this when we get around to specifying how type promotion works with patterns.

stereotype441 avatar Sep 13 '22 22:09 stereotype441

Thanks for filing this. There's also this unfilled section in the proposal whose body is just a TODO. :)

munificent avatar Sep 13 '22 23:09 munificent

I definitely want this to work.

A pattern of Foo(...) should promote the scrutinee, if it's a local variable, to Foo. (If it's a promotion, so Foo should be a subtype of the current type of the variable). Same for [...] to List (and <int>[...] to List<int>), {"foo": ...} to Map, and record patterns to the record type.

Also, p? and p! should both promote to non-null, as X should promote to X.

I also want the when clause to promote the same way as a normal conditional expression. After all if (0 case _ when ...) ... is completely equivalent to if (...) ....

lrhn avatar Sep 14 '22 07:09 lrhn

Related idea: once we have field promotion (which we should have before we have patterns) we may want extractor patterns to promote fields, e.g.:

class C {
  final int? _i;
  ...
}
foo(Object x) {
  switch (x) {
    case C(_i: _!): // Promotes x._i to non-nullable `int`
      print(x._i + 1); // Ok; `x._i` has been promoted.
  }
}

Not hugely useful, but it would be consistent with everything else we're doing.

stereotype441 avatar Sep 14 '22 17:09 stereotype441

Have thought about this more.

A pattern match should be treated as if it has an implicit variable containing the value being matched, and the checks of the pattern can promote that variable. If the value comes from a switch (x) on an existing local variable, use that variable, otherwise introduce a new anonymous variable instead.

A successful pattern match can then promote the variable just as a successful is check (or != null check for ?/! patterns).

Pattern Promotion of implicit variable x
p1 | p2 the merge of the promotions of p1 and p2
p1 & p2 promote as p1, then as p2
p1? promote like x != null then as p1
p1! promote like x! then as p1
p1 as type promote like x as type, then as p1
[p1, p2] promote like x is List
var id no promotion, static type of id is the current promoted type of the scrutinee variable
final id no promotion, static type of id is the current promoted type of the scrutinee variable
_ no promoation
final? type id promote like x is type
<T>[p1, p2] promote like x is List<T>
{k1: p1, j2: p2} promote like x is Map
<K,V>[k1: p1, k2: p2] promote like x is Map<K, V>
Foo(x1: p1, y1: p1) promote like x is Foo
Foo<T>(x1: p1, y1: p1) promote like x is Foo<T>
(p1, p2) promote like x is (Object?, Object?), then let x1 = .$1 and promote x1 by p1,
similar for p2, then take the T1, T2, the promoted types for x1, x2 and promote x to (T1, T2).

The "merge" here is what we'd do for a conditional if (test) { something } else { other } where something promotes like p1 and other promotes like p2, then figure out what the promoted type of x is afterwards.

The record case is special in that the destructured fields directly relate to the type of the record. Doing case (int x, int y) => should promote the incoming variable to (int, int). We can't do the same thing for case [int x, int y] => because we don't know that the type of the list is List<int>. It can be a List<Object> and it would be an unsound promotion to promote it to List<int> based only on its content. That's not the case for records, their type always matches one-to-one with the types of their content, because that's how a record's type is defined.

A when guard clause of a casehead works like a normal condition, which is "and"'ed to the promotions of the pattern, and can promote on its positive (and possibly negative) branches.

Examples:

switch (someObject as Object?) {
  case (int x, int y) & var p:  // p has type (int, int).
  case (num x, num y) & var p & (int _, int_):  // p has type (int, int).
  case var x when x is int: x has declared type Object?, promoted type `int`
}

Object? x = ...;
switch (x) {
  case int(sign: var s): // x has promoted type `int` and `s` has type `int`.
  case (num _, num _) & var p & (int x, int y): // p has type (num, num), x has promoted type (int, int)
  case (num f, int _) | (num f, double _): // x (maybe) has type (num, Object) or (num, num), f has type num.
}

About the negative edge promotion, we can allow:

int? value = ...;
switch (value) {
  case null: // default case
  case var n: // n has type int
}

Even:

FutureOr<int> f = ...;l
switch (f) {
  case Future<int> f: return 1 + await f;
  case var value:  return 1 + value // Have we promoted f to `int` before declaring `value`?
}

We can also do promotion along the negative edge of a when guard, at least if the pattern before it is irrefutable. I can't find any reasonable way to use that, though:

Object? value = ...;
if (value case var v when value == null || complexTest(v)) throw "bad";
// value is not null here.

(But why?!) If the test can promote, it can also be incorporated into the pattern, so doing it in the when clause would mean that it's a complicated non-promoting test anyway.

Still, whatever we do, if we do it properly, such behavior should just fall out of the flow/promotion analysis

lrhn avatar Sep 15 '22 14:09 lrhn

Matching does promote the matched value variable (at least in some cases), so I'm going to consider this done. :)

munificent avatar Apr 07 '23 00:04 munificent