diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index dc90bb085f..78da238aee 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -1017,6 +1017,7 @@ pub fn (mut c Checker) call_method(mut call_expr ast.CallExpr) table.Type { call_expr.left_type = left_type left_type_sym := c.table.get_type_symbol(c.unwrap_generic(left_type)) method_name := call_expr.name + mut unknown_method_msg := 'unknown method: `${left_type_sym.source_name}.$method_name`' if left_type.has_flag(.optional) { c.error('optional type cannot be called directly', call_expr.left.position()) return table.void_type @@ -1213,6 +1214,11 @@ pub fn (mut c Checker) call_method(mut call_expr ast.CallExpr) table.Type { } call_expr.return_type = method.return_type return method.return_type + } else { + if left_type_sym.kind == .aggregate { + // the error message contains the problematic type + unknown_method_msg = err + } } // TODO: str methods if method_name == 'str' { @@ -1246,8 +1252,7 @@ pub fn (mut c Checker) call_method(mut call_expr ast.CallExpr) table.Type { } if left_type != table.void_type { suggestion := util.new_suggestion(method_name, left_type_sym.methods.map(it.name)) - c.error(suggestion.say('unknown method: `${left_type_sym.source_name}.$method_name`'), - call_expr.pos) + c.error(suggestion.say(unknown_method_msg), call_expr.pos) } return table.void_type } @@ -1651,19 +1656,24 @@ pub fn (mut c Checker) selector_expr(mut selector_expr ast.SelectorExpr) table.T return table.int_type } } + mut unknown_field_msg := 'type `$sym.source_name` has no field or method `$field_name`' if field := c.table.struct_find_field(sym, field_name) { if sym.mod != c.mod && !field.is_pub { c.error('field `${sym.source_name}.$field_name` is not public', selector_expr.pos) } selector_expr.typ = field.typ return field.typ + } else { + if sym.kind == .aggregate { + unknown_field_msg = err + } } - if sym.kind != .struct_ { + if sym.kind !in [.struct_, .aggregate] { if sym.kind != .placeholder { c.error('`$sym.source_name` is not a struct', selector_expr.pos) } } else { - c.error('type `$sym.source_name` has no field or method `$field_name`', selector_expr.pos) + c.error(unknown_field_msg, selector_expr.pos) } return table.void_type } diff --git a/vlib/v/checker/tests/match_sumtype_multiple_types.out b/vlib/v/checker/tests/match_sumtype_multiple_types.out new file mode 100644 index 0000000000..7ff5890754 --- /dev/null +++ b/vlib/v/checker/tests/match_sumtype_multiple_types.out @@ -0,0 +1,14 @@ +vlib/v/checker/tests/match_sumtype_multiple_types.vv:25:13: error: type `Charlie` has no field or method `char` + 23 | match NATOAlphabet(a) as l { + 24 | Alfa, Charlie { + 25 | assert l.char == `a` + | ~~~~ + 26 | assert l.letter() == 'a' + 27 | } +vlib/v/checker/tests/match_sumtype_multiple_types.vv:26:13: error: unknown method: `Charlie.letter` + 24 | Alfa, Charlie { + 25 | assert l.char == `a` + 26 | assert l.letter() == 'a' + | ~~~~~~~~~ + 27 | } + 28 | Bravo { diff --git a/vlib/v/checker/tests/match_sumtype_multiple_types.vv b/vlib/v/checker/tests/match_sumtype_multiple_types.vv new file mode 100644 index 0000000000..6b7ee6c0f4 --- /dev/null +++ b/vlib/v/checker/tests/match_sumtype_multiple_types.vv @@ -0,0 +1,32 @@ +struct Alfa { + char rune +} + +fn (a Alfa) letter() rune { + return a.char +} + +struct Bravo { + char rune +} + +fn (b Bravo) letter() rune { + return b.char +} + +struct Charlie {} + +type NATOAlphabet = Alfa | Bravo | Charlie + +fn method_not_exists() { + a := Alfa{} + match NATOAlphabet(a) as l { + Alfa, Charlie { + assert l.char == `a` + assert l.letter() == 'a' + } + Bravo { + assert false + } + } +} diff --git a/vlib/v/gen/js/js.v b/vlib/v/gen/js/js.v index 545f14bcb4..3433096889 100644 --- a/vlib/v/gen/js/js.v +++ b/vlib/v/gen/js/js.v @@ -283,6 +283,9 @@ pub fn (mut g JsGen) typ(t table.Type) string { .rune { styp = 'any' } + .aggregate { + panic('TODO: unhandled aggregate in JS') + } } /* else { diff --git a/vlib/v/parser/if_match.v b/vlib/v/parser/if_match.v index 90143d7182..20c0da30b2 100644 --- a/vlib/v/parser/if_match.v +++ b/vlib/v/parser/if_match.v @@ -6,6 +6,7 @@ module parser import v.ast import v.table import v.token +import strings fn (mut p Parser) if_expr(is_comptime bool) ast.IfExpr { was_inside_if_expr := p.inside_if_expr @@ -210,14 +211,48 @@ fn (mut p Parser) match_expr() ast.MatchExpr { } } } - // Sum type match - typ := p.parse_type() - exprs << ast.Type{ - typ: typ + mut types := []table.Type{} + for { + // Sum type match + parsed_type := p.parse_type() + types << parsed_type + exprs << ast.Type{ + typ: parsed_type + } + if p.tok.kind != .comma { + break + } + p.check(.comma) + } + mut it_typ := table.void_type + if types.len == 1 { + it_typ = types[0] + } else { + // there is more than one types, so we must create a type aggregate + mut agg_name := strings.new_builder(20) + agg_name.write('(') + for i, typ in types { + if i > 0 { + agg_name.write(' | ') + } + type_str := p.table.type_to_str(typ) + agg_name.write(p.prepend_mod(type_str)) + } + agg_name.write(')') + name := agg_name.str() + it_typ = p.table.register_type_symbol(table.TypeSymbol{ + name: name + source_name: name + kind: .aggregate + mod: p.mod + info: table.Aggregate{ + types: types + } + }) } p.scope.register('it', ast.Var{ name: 'it' - typ: typ.to_ptr() + typ: it_typ.to_ptr() pos: cond_pos is_used: true is_mut: is_mut @@ -226,18 +261,13 @@ fn (mut p Parser) match_expr() ast.MatchExpr { // Register shadow variable or `as` variable with actual type p.scope.register(var_name, ast.Var{ name: var_name - typ: typ.to_ptr() + typ: it_typ.to_ptr() pos: cond_pos is_used: true is_changed: true // TODO mut unchanged warning hack, remove is_mut: is_mut }) } - // TODO - if p.tok.kind == .comma { - p.next() - p.parse_type() - } is_sum_type = true } else { // Expression match diff --git a/vlib/v/table/atypes.v b/vlib/v/table/atypes.v index c4db69f371..15e6eea093 100644 --- a/vlib/v/table/atypes.v +++ b/vlib/v/table/atypes.v @@ -16,7 +16,7 @@ import strings pub type Type = int pub type TypeInfo = Alias | Array | ArrayFixed | Chan | Enum | FnType | GenericStructInst | - Interface | Map | MultiReturn | Struct | SumType + Interface | Map | MultiReturn | Struct | SumType | Aggregate pub enum Language { v @@ -366,6 +366,7 @@ pub enum Kind { interface_ any_float any_int + aggregate } pub fn (t &TypeSymbol) str() string { @@ -681,6 +682,7 @@ pub fn (k Kind) str() string { .ustring { 'ustring' } .generic_struct_inst { 'generic_struct_inst' } .rune { 'rune' } + .aggregate { 'aggregate' } } return k_str } @@ -730,6 +732,13 @@ pub: language Language } +pub struct Aggregate { +mut: + fields []Field // used for faster lookup inside the module +pub: + types []Type +} + // NB: FExpr here is a actually an ast.Expr . // It should always be used by casting to ast.Expr, using ast.fe2ex()/ast.ex2fe() // That hack is needed to break an import cycle between v.ast and v.table . @@ -749,6 +758,15 @@ pub mut: is_global bool } +fn (f &Field) equals(o &Field) bool { + return f.name == o.name && + f.typ == o.typ && + // TODO Should those be checked ? + f.is_pub == o.is_pub && + f.is_mut == o.is_mut && + f.is_global == o.is_global +} + pub struct Array { pub: nr_dims int @@ -899,6 +917,15 @@ pub fn (t &TypeSymbol) str_method_info() (bool, bool, int) { return has_str_method, expects_ptr, nr_args } +fn (a &Aggregate) find_field(name string) ?Field { + for field in a.fields { + if field.name == name { + return field + } + } + return none +} + pub fn (s Struct) find_field(name string) ?Field { for field in s.fields { if field.name == name { diff --git a/vlib/v/table/table.v b/vlib/v/table/table.v index ef5bacbcbb..006ccd8f91 100644 --- a/vlib/v/table/table.v +++ b/vlib/v/table/table.v @@ -39,6 +39,18 @@ pub mut: name string } +fn (f &Fn) method_equals(o &Fn) bool { + return f.params[1..].equals(o.params[1..]) && + f.return_type == o.return_type && + f.return_type_source_name == o.return_type_source_name && + f.is_variadic == o.is_variadic && + f.language == o.language && + f.is_generic == o.is_generic && + f.is_pub == o.is_pub && + f.mod == o.mod && + f.name == o.name +} + pub struct Param { pub: pos token.Position @@ -49,6 +61,26 @@ pub: is_hidden bool // interface first arg } +fn (p &Param) equals(o &Param) bool { + return p.name == o.name + && p.is_mut == o.is_mut + && p.typ == o.typ + && p.type_source_name == o.type_source_name + && p.is_hidden == o.is_hidden +} + +fn (p []Param) equals(o []Param) bool { + if p.len != o.len { + return false + } + for i in 0..p.len { + if !p[i].equals(o[i]) { + return false + } + } + return true +} + pub struct Var { pub: name string @@ -139,6 +171,32 @@ pub fn (mut t TypeSymbol) register_method(new_fn Fn) { t.methods << new_fn } +pub fn (t &Table) register_aggregate_method(mut sym TypeSymbol, name string) ?Fn { + if sym.kind != .aggregate { + panic('Unexpected type symbol: $sym.kind') + } + agg_info := sym.info as Aggregate + // an aggregate always has at least 2 types + mut found_once := false + mut new_fn := Fn{} + for typ in agg_info.types { + ts := t.get_type_symbol(typ) + if type_method := ts.find_method(name) { + if !found_once { + found_once = true + new_fn = type_method + } else if !new_fn.method_equals(type_method) { + return error('method `${t.type_to_str(typ)}.$name` signature is different') + } + } else { + return error('unknown method: `${t.type_to_str(typ)}.$name`') + } + } + // register the method in the aggregate, so lookup is faster next time + sym.register_method(new_fn) + return new_fn +} + pub fn (t &Table) type_has_method(s &TypeSymbol, name string) bool { // println('type_has_method($s.name, $name) types.len=$t.types.len s.parent_idx=$s.parent_idx') if _ := t.type_find_method(s, name) { @@ -155,6 +213,10 @@ pub fn (t &Table) type_find_method(s &TypeSymbol, name string) ?Fn { if method := ts.find_method(name) { return method } + if ts.kind == .aggregate { + method := t.register_aggregate_method(mut ts, name) ? + return method + } if ts.parent_idx == 0 { break } @@ -163,6 +225,31 @@ pub fn (t &Table) type_find_method(s &TypeSymbol, name string) ?Fn { return none } +fn (t &Table) register_aggregate_field(mut sym TypeSymbol, name string) ?Field { + if sym.kind != .aggregate { + panic('Unexpected type symbol: $sym.kind') + } + mut agg_info := sym.info as Aggregate + // an aggregate always has at least 2 types + mut found_once := false + mut new_field := Field{} + for typ in agg_info.types { + ts := t.get_type_symbol(typ) + if type_field := t.struct_find_field(ts, name) { + if !found_once { + found_once = true + new_field = type_field + } else if !new_field.equals(type_field) { + return error('field `${t.type_to_str(typ)}.$name` type is different') + } + } else { + return error('type `${t.type_to_str(typ)}` has no field or method `$name`') + } + } + agg_info.fields << new_field + return new_field +} + pub fn (t &Table) struct_has_field(s &TypeSymbol, name string) bool { // println('struct_has_field($s.name, $name) types.len=$t.types.len s.parent_idx=$s.parent_idx') if _ := t.struct_find_field(s, name) { @@ -176,11 +263,18 @@ pub fn (t &Table) struct_find_field(s &TypeSymbol, name string) ?Field { // println('struct_find_field($s.name, $name) types.len=$t.types.len s.parent_idx=$s.parent_idx') mut ts := s for { - if ts.info is Struct { - struct_info := ts.info as Struct + if ts.info is Struct as struct_info { if field := struct_info.find_field(name) { return field } + } else if ts.info is Aggregate as agg_info { + if field := agg_info.find_field(name) { + return field + } + field := t.register_aggregate_field(mut ts, name) or { + return error(err) + } + return field } if ts.parent_idx == 0 { break @@ -396,7 +490,7 @@ pub fn (t &Table) map_source_name(key_type, value_type Type) string { key_type_sym := t.get_type_symbol(key_type) value_type_sym := t.get_type_symbol(value_type) ptr := if value_type.is_ptr() { '&' } else { '' } - return 'map[${key_type_sym.source_name}]$ptr$value_type_sym.source_name' + return 'map[$key_type_sym.source_name]$ptr$value_type_sym.source_name' } pub fn (mut t Table) find_or_register_chan(elem_type Type, is_mut bool) int { @@ -415,7 +509,7 @@ pub fn (mut t Table) find_or_register_chan(elem_type Type, is_mut bool) int { source_name: source_name info: Chan{ elem_type: elem_type - is_mut: is_mut + is_mut: is_mut } } return t.register_type_symbol(chan_typ) diff --git a/vlib/v/tests/match_test.v b/vlib/v/tests/match_test.v index 80e234e215..0b12c98e9a 100644 --- a/vlib/v/tests/match_test.v +++ b/vlib/v/tests/match_test.v @@ -164,3 +164,40 @@ fn test_sum_type_name() { } assert f(a) == 'A1' } + +struct Alfa { + char rune = `a` +} + +fn (a Alfa) letter() rune { + return a.char +} + +struct Bravo { + // A field so that Alfa and Bravo structures aren't the same + dummy_field int + char rune = `b` +} + +fn (b Bravo) letter() rune { + return b.char +} + +struct Charlie {} + +type NATOAlphabet = Alfa | Bravo | Charlie + +fn test_match_sum_type_multiple_type() { + a := Alfa{} + // TODO This currently works because cgen takes the first type as the type of `l` + // it would fail if we `a` was of type `Bravo` + match NATOAlphabet(a) as l { + Alfa, Bravo { + assert l.char == `a` + assert l.letter() == `a` + } + Charlie { + assert false + } + } +}