ballerina-lang
ballerina-lang copied to clipboard
Deviations in type narrowing with the match statement
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