ballerina-lang icon indicating copy to clipboard operation
ballerina-lang copied to clipboard

Deviations in type narrowing with the match statement

Open MaryamZi opened this issue 4 years ago • 3 comments
trafficstars

Description: $title. What we've implemented doesn't seem to be in-line with the spec.

The spec says

A variable is the matched variable of a match-stmt if the action-or-expression following match is a variable-reference that references this variable. The type of a matched variable is subject to narrowing within regions of the match-stmt as follows. The matched variable has a narrowed type at the start of each match-clause, which is determined by previous match-clauses; the type for the first match-clause is the normal static type of the variable. Narrowing is applied to each match-clause as follows:

  • let T be the narrowed type of the matched variable at the start of the match-clause;
  • let P be the union of the corresponding types of each match-pattern in the match-pattern-list of the match-clause;
  • let N be the intersection of T and P;
  • let R be the set difference of T and P;
  • if N is a subtype of readonly, then the narrowed type for the match-guard, if any, and statement-block of the match-clause is N; otherwise it is T;
  • if there is a match-guard, then the type narrowing described in Conditional variable type narrowing is done in addition to the type narrowing described here;
  • if R is a subtype of readonly and there is no match-guard, then the narrowed type for the next match-clause is R; otherwise, it is T.

Steps to reproduce: Case I

type Foo record {|
    readonly int a;
|};

type Bar record {|
    readonly string b;
|};

function func(Foo|Bar fb) {
    match fb {
        // `T` here is `Foo|Bar`
        {a: var a} => { // `P` is record {| any|error a; any|error...; |}, `N` is `Foo`, `R` is `Bar`
            int i = a; 
            int j = fb.a; // since `N` is a subtype of `readonly`, type should be narrowed
        }
        // `R` is a subtype of `readonly` and there is no match guard, therefore the type for the
        // narrowed type for the next match clause should be `R` - `Bar`
        {...var rest} => {
            map<string> k = rest; // Since `Bar` is matched to `{...var rest}`, `rest` will have only a `string` field
            string l = fb.b; // valid since type should be narrowed to `Bar`
        }
    }
}

slalpha4

ERROR [foo.bal:(13:21,13:22)] incompatible types: expected 'int', found '(any|error)'
ERROR [foo.bal:(14:21,14:25)] invalid operation: type '(Foo|Bar)' does not support field access for non-required field 'a'
ERROR [foo.bal:(19:29,19:33)] incompatible types: expected 'map<string>', found 'map<(other|int|other|string)>'
ERROR [foo.bal:(20:24,20:28)] invalid operation: type '(Foo|Bar)' does not support field access for non-required field 'b'

slalpha5-SNAPSHOT

ERROR [foo.bal:(14:21,14:25)] invalid operation: type '(Foo|Bar)' does not support field access for non-required field 'a'
ERROR [foo.bal:(19:29,19:33)] incompatible types: expected 'map<string>', found 'map<(other|int|other|string)>'

Case II

type Person record { 
    string name; 
};

function func(Person|error v) returns string? {
    match v {
        // `T` here is `Person|error`
        error() => { // `P` is `error`, `N` is `error` and `R` is `Person`
            // return v.message(); // since `N` is a subtype of `readonly`, type should be narrowed
        }
        var val => {
            string n = val.name;
            return v.name;
        }
    }
}
ERROR [foo.bal:(12:24,12:32)] invalid operation: type '(Person|error)' does not support field access

Right now, v.name seems to work, but I don't think this is allowed according to the current spec, because R is not a subtype of readonly.

Affected Versions: slalpha4

MaryamZi avatar Apr 24 '21 05:04 MaryamZi