all: inline sum types (#12912)

pull/12963/head
Daniel Däschle 2021-12-25 16:26:40 +01:00 committed by GitHub
parent 485b392cb3
commit 35282396ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 312 additions and 114 deletions

View File

@ -1083,7 +1083,7 @@ pub:
comments []Comment
}
// New implementation of sum types
// SumTypeDecl is the ast node for `type MySumType = string | int`
pub struct SumTypeDecl {
pub:
name string

View File

@ -918,6 +918,7 @@ pub:
pub mut:
fields []StructField
found_fields bool
is_anon bool
// generic sumtype support
is_generic bool
generic_types []Type
@ -1048,6 +1049,10 @@ pub fn (t &Table) type_to_str_using_aliases(typ Type, import_aliases map[string]
}
else {}
}
} else if sym.info is SumType && (sym.info as SumType).is_anon {
variant_names := sym.info.variants.map(t.shorten_user_defined_typenames(t.sym(it).name,
import_aliases))
res = '${variant_names.join(' | ')}'
} else {
res = t.shorten_user_defined_typenames(res, import_aliases)
}
@ -1089,7 +1094,7 @@ pub fn (t &Table) type_to_str_using_aliases(typ Type, import_aliases map[string]
res = strings.repeat(`&`, nr_muls) + res
}
if typ.has_flag(.optional) {
res = '?' + res
res = '?$res'
}
return res
}

View File

@ -0,0 +1,13 @@
import v.token
struct Foo {
bar string | int
}
interface Egg {
milk string | int
}
fn foo(bar string | int) int | string | token.Position {
return 1
}

View File

@ -434,16 +434,22 @@ fn (mut g Gen) gen_str_for_union_sum_type(info ast.SumType, styp string, str_fn_
g.type_definitions.writeln('static string indent_${str_fn_name}($styp x, int indent_count); // auto')
mut fn_builder := strings.new_builder(512)
fn_builder.writeln('static string indent_${str_fn_name}($styp x, int indent_count) {')
mut clean_sum_type_v_type_name := styp.replace('__', '.')
if styp.ends_with('*') {
clean_sum_type_v_type_name = '&' + clean_sum_type_v_type_name.replace('*', '')
mut clean_sum_type_v_type_name := ''
if info.is_anon {
variant_names := info.variants.map(g.table.sym(it).name)
clean_sum_type_v_type_name = '(${variant_names.join(' | ')})'
} else {
clean_sum_type_v_type_name = styp.replace('__', '.')
if styp.ends_with('*') {
clean_sum_type_v_type_name = '&' + clean_sum_type_v_type_name.replace('*', '')
}
if clean_sum_type_v_type_name.contains('_T_') {
clean_sum_type_v_type_name =
clean_sum_type_v_type_name.replace('Array_', '[]').replace('_T_', '<').replace('_', ', ') +
'>'
}
clean_sum_type_v_type_name = util.strip_main_name(clean_sum_type_v_type_name)
}
if clean_sum_type_v_type_name.contains('_T_') {
clean_sum_type_v_type_name =
clean_sum_type_v_type_name.replace('Array_', '[]').replace('_T_', '<').replace('_', ', ') +
'>'
}
clean_sum_type_v_type_name = util.strip_main_name(clean_sum_type_v_type_name)
fn_builder.writeln('\tswitch(x._typ) {')
for typ in info.variants {
typ_str := g.typ(typ)

View File

@ -539,6 +539,10 @@ fn (mut p Parser) fn_decl() ast.FnDecl {
}
fn (mut p Parser) fn_receiver(mut params []ast.Param, mut rec ReceiverParsingInfo) ? {
p.inside_receiver_param = true
defer {
p.inside_receiver_param = false
}
lpar_pos := p.tok.position()
p.next() // (
is_shared := p.tok.kind == .key_shared

View File

@ -8,6 +8,10 @@ import v.ast
import v.util
import v.token
const (
maximum_inline_sum_type_variants = 3
)
pub fn (mut p Parser) parse_array_type(expecting token.Kind) ast.Type {
p.check(expecting)
// fixed array
@ -283,9 +287,74 @@ pub fn (mut p Parser) parse_language() ast.Language {
return language
}
// parse_inline_sum_type parses the type and registers it in case the type is an anonymous sum type.
// It also takes care of inline sum types where parse_type only parses a standalone type.
pub fn (mut p Parser) parse_inline_sum_type() ast.Type {
variants := p.parse_sum_type_variants()
if variants.len > 1 {
if variants.len > parser.maximum_inline_sum_type_variants {
pos := variants[0].pos.extend(variants[variants.len - 1].pos)
p.warn_with_pos('an inline sum type expects a maximum of $parser.maximum_inline_sum_type_variants types ($variants.len were given)',
pos)
}
mut variant_names := variants.map(p.table.sym(it.typ).name)
variant_names.sort()
// deterministic name
name := '_v_anon_sum_type_${variant_names.join('_')}'
variant_types := variants.map(it.typ)
prepend_mod_name := p.prepend_mod(name)
mut idx := p.table.find_type_idx(prepend_mod_name)
if idx > 0 {
return ast.new_type(idx)
}
idx = p.table.register_type_symbol(ast.TypeSymbol{
kind: .sum_type
name: prepend_mod_name
cname: util.no_dots(prepend_mod_name)
mod: p.mod
info: ast.SumType{
is_anon: true
variants: variant_types
}
})
return ast.new_type(idx)
} else if variants.len == 1 {
return variants[0].typ
}
return ast.Type(0)
}
// parse_sum_type_variants parses several types separated with a pipe and returns them as a list with at least one node.
// If there is less than one node, it will add an error to the error list.
pub fn (mut p Parser) parse_sum_type_variants() []ast.TypeNode {
p.inside_sum_type = true
defer {
p.inside_sum_type = false
}
mut types := []ast.TypeNode{}
for {
type_start_pos := p.tok.position()
typ := p.parse_type()
// TODO: needs to be its own var, otherwise TCC fails because of a known stack error
prev_tok := p.prev_tok
type_end_pos := prev_tok.position()
type_pos := type_start_pos.extend(type_end_pos)
types << ast.TypeNode{
typ: typ
pos: type_pos
}
if p.tok.kind != .pipe {
break
}
p.check(.pipe)
}
return types
}
pub fn (mut p Parser) parse_type() ast.Type {
// optional
mut is_optional := false
optional_pos := p.tok.position()
if p.tok.kind == .question {
line_nr := p.tok.line_nr
p.next()
@ -333,6 +402,10 @@ pub fn (mut p Parser) parse_type() ast.Type {
p.error_with_pos('use `?` instead of `?void`', pos)
return 0
}
sym := p.table.sym(typ)
if is_optional && sym.info is ast.SumType && (sym.info as ast.SumType).is_anon {
p.error_with_pos('an inline sum type cannot be optional', optional_pos.extend(p.prev_tok.position()))
}
}
if is_optional {
typ = typ.set_flag(.optional)
@ -363,7 +436,6 @@ pub fn (mut p Parser) parse_any_type(language ast.Language, is_ptr bool, check_d
name = 'JS.$name'
} else if p.peek_tok.kind == .dot && check_dot {
// `module.Type`
// /if !(p.tok.lit in p.table.imports) {
mut mod := name
mut mod_pos := p.tok.position()
p.next()
@ -394,7 +466,7 @@ pub fn (mut p Parser) parse_any_type(language ast.Language, is_ptr bool, check_d
p.error('imported types must start with a capital letter')
return 0
}
} else if p.expr_mod != '' && !p.in_generic_params { // p.expr_mod is from the struct and not from the generic parameter
} else if p.expr_mod != '' && !p.inside_generic_params { // p.expr_mod is from the struct and not from the generic parameter
name = p.expr_mod + '.' + name
} else if name in p.imported_symbols {
name = p.imported_symbols[name]
@ -402,7 +474,6 @@ pub fn (mut p Parser) parse_any_type(language ast.Language, is_ptr bool, check_d
// `Foo` in module `mod` means `mod.Foo`
name = p.mod + '.' + name
}
// p.warn('get type $name')
match p.tok.kind {
.key_fn {
// func
@ -412,16 +483,19 @@ pub fn (mut p Parser) parse_any_type(language ast.Language, is_ptr bool, check_d
// array
return p.parse_array_type(p.tok.kind)
}
.lpar {
// multiple return
if is_ptr {
p.error('parse_type: unexpected `&` before multiple returns')
return 0
}
return p.parse_multi_return_type()
}
else {
// no p.next()
if p.tok.kind == .lpar && !p.inside_sum_type {
// multiple return
if is_ptr {
p.error('parse_type: unexpected `&` before multiple returns')
return 0
}
return p.parse_multi_return_type()
}
if ((p.peek_tok.kind == .dot && p.peek_token(3).kind == .pipe)
|| p.peek_tok.kind == .pipe) && !p.inside_sum_type && !p.inside_receiver_param {
return p.parse_inline_sum_type()
}
if name == 'map' {
return p.parse_map_type()
}
@ -541,7 +615,7 @@ pub fn (mut p Parser) parse_generic_inst_type(name string) ast.Type {
mut bs_cname := name
start_pos := p.tok.position()
p.next()
p.in_generic_params = true
p.inside_generic_params = true
bs_name += '<'
bs_cname += '_T_'
mut concrete_types := []ast.Type{}
@ -564,7 +638,7 @@ pub fn (mut p Parser) parse_generic_inst_type(name string) ast.Type {
}
concrete_types_pos := start_pos.extend(p.tok.position())
p.check(.gt)
p.in_generic_params = false
p.inside_generic_params = false
bs_name += '>'
// fmt operates on a per-file basis, so is_instance might be not set correctly. Thus it's ignored.
if (is_instance || p.pref.is_fmt) && concrete_types.len > 0 {

View File

@ -26,64 +26,66 @@ mut:
scanner &scanner.Scanner
comments_mode scanner.CommentsMode = .skip_comments
// see comment in parse_file
tok token.Token
prev_tok token.Token
peek_tok token.Token
table &ast.Table
language ast.Language
fn_language ast.Language // .c for `fn C.abcd()` declarations
inside_vlib_file bool // true for all vlib/ files
inside_test_file bool // when inside _test.v or _test.vv file
inside_if bool
inside_if_expr bool
inside_ct_if_expr bool
inside_or_expr bool
inside_for bool
inside_fn bool // true even with implicit main
inside_unsafe_fn bool
inside_str_interp bool
inside_array_lit bool
inside_in_array bool
or_is_handled bool // ignore `or` in this expression
builtin_mod bool // are we in the `builtin` module?
mod string // current module name
is_manualfree bool // true when `[manualfree] module abc`, makes *all* fns in the current .v file, opt out of autofree
has_globals bool // `[has_globals] module abc` - allow globals declarations, even without -enable-globals, in that single .v file __only__
is_generated bool // `[generated] module abc` - turn off compiler notices for that single .v file __only__.
attrs []ast.Attr // attributes before next decl stmt
expr_mod string // for constructing full type names in parse_type()
scope &ast.Scope
imports map[string]string // alias => mod_name
ast_imports []ast.Import // mod_names
used_imports []string // alias
auto_imports []string // imports, the user does not need to specify
imported_symbols map[string]string
is_amp bool // for generating the right code for `&Foo{}`
returns bool
inside_match bool // to separate `match A { }` from `Struct{}`
inside_select bool // to allow `ch <- Struct{} {` inside `select`
inside_match_case bool // to separate `match_expr { }` from `Struct{}`
inside_match_body bool // to fix eval not used TODO
inside_unsafe bool
is_stmt_ident bool // true while the beginning of a statement is an ident/selector
expecting_type bool // `is Type`, expecting type
errors []errors.Error
warnings []errors.Warning
notices []errors.Notice
vet_errors []vet.Error
cur_fn_name string
label_names []string
in_generic_params bool // indicates if parsing between `<` and `>` of a method/function
name_error bool // indicates if the token is not a name or the name is on another line
n_asm int // controls assembly labels
inside_asm_template bool
inside_asm bool
global_labels []string
inside_defer bool
comptime_if_cond bool
defer_vars []ast.Ident
should_abort bool // when too many errors/warnings/notices are accumulated, should_abort becomes true, and the parser should stop
codegen_text string
tok token.Token
prev_tok token.Token
peek_tok token.Token
table &ast.Table
language ast.Language
fn_language ast.Language // .c for `fn C.abcd()` declarations
inside_vlib_file bool // true for all vlib/ files
inside_test_file bool // when inside _test.v or _test.vv file
inside_if bool
inside_if_expr bool
inside_ct_if_expr bool
inside_or_expr bool
inside_for bool
inside_fn bool // true even with implicit main
inside_unsafe_fn bool
inside_str_interp bool
inside_array_lit bool
inside_in_array bool
inside_match bool // to separate `match A { }` from `Struct{}`
inside_select bool // to allow `ch <- Struct{} {` inside `select`
inside_match_case bool // to separate `match_expr { }` from `Struct{}`
inside_match_body bool // to fix eval not used TODO
inside_unsafe bool
inside_sum_type bool // to prevent parsing inline sum type again
inside_asm_template bool
inside_asm bool
inside_defer bool
inside_generic_params bool // indicates if parsing between `<` and `>` of a method/function
inside_receiver_param bool // indicates if parsing the receiver parameter inside the first `(` and `)` of a method
or_is_handled bool // ignore `or` in this expression
builtin_mod bool // are we in the `builtin` module?
mod string // current module name
is_manualfree bool // true when `[manualfree] module abc`, makes *all* fns in the current .v file, opt out of autofree
has_globals bool // `[has_globals] module abc` - allow globals declarations, even without -enable-globals, in that single .v file __only__
is_generated bool // `[generated] module abc` - turn off compiler notices for that single .v file __only__.
attrs []ast.Attr // attributes before next decl stmt
expr_mod string // for constructing full type names in parse_type()
scope &ast.Scope
imports map[string]string // alias => mod_name
ast_imports []ast.Import // mod_names
used_imports []string // alias
auto_imports []string // imports, the user does not need to specify
imported_symbols map[string]string
is_amp bool // for generating the right code for `&Foo{}`
returns bool
is_stmt_ident bool // true while the beginning of a statement is an ident/selector
expecting_type bool // `is Type`, expecting type
errors []errors.Error
warnings []errors.Warning
notices []errors.Notice
vet_errors []vet.Error
cur_fn_name string
label_names []string
name_error bool // indicates if the token is not a name or the name is on another line
n_asm int // controls assembly labels
global_labels []string
comptime_if_cond bool
defer_vars []ast.Ident
should_abort bool // when too many errors/warnings/notices are accumulated, should_abort becomes true, and the parser should stop
codegen_text string
}
// for tests
@ -3419,32 +3421,16 @@ fn (mut p Parser) type_decl() ast.TypeDecl {
comments: comments
}
}
first_type := p.parse_type() // need to parse the first type before we can check if it's `type A = X | Y`
type_alias_pos := p.tok.position()
if p.tok.kind == .pipe {
mut type_end_pos := p.prev_tok.position()
type_pos = type_pos.extend(type_end_pos)
p.next()
sum_variants << ast.TypeNode{
typ: first_type
pos: type_pos
}
// type SumType = A | B | c
for {
type_pos = p.tok.position()
variant_type := p.parse_type()
// TODO: needs to be its own var, otherwise TCC fails because of a known stack error
prev_tok := p.prev_tok
type_end_pos = prev_tok.position()
type_pos = type_pos.extend(type_end_pos)
sum_variants << ast.TypeNode{
typ: variant_type
pos: type_pos
sum_variants << p.parse_sum_type_variants()
// type SumType = A | B | c
if sum_variants.len > 1 {
for variant in sum_variants {
variant_sym := p.table.sym(variant.typ)
// TODO: implement this check for error too
if variant_sym.kind == .none_ {
p.error_with_pos('named sum type cannot have none as its variant', variant.pos)
return ast.AliasTypeDecl{}
}
if p.tok.kind != .pipe {
break
}
p.check(.pipe)
}
variant_types := sum_variants.map(it.typ)
prepend_mod_name := p.prepend_mod(name)
@ -3481,7 +3467,8 @@ fn (mut p Parser) type_decl() ast.TypeDecl {
p.error_with_pos('generic type aliases are not yet implemented', decl_pos_with_generics)
return ast.AliasTypeDecl{}
}
parent_type := first_type
// sum_variants will have only one element
parent_type := sum_variants[0].typ
parent_sym := p.table.sym(parent_type)
pidx := parent_type.idx()
p.check_for_impure_v(parent_sym.language, decl_pos)
@ -3505,6 +3492,7 @@ fn (mut p Parser) type_decl() ast.TypeDecl {
return ast.AliasTypeDecl{}
}
if idx == pidx {
type_alias_pos := sum_variants[0].pos
p.error_with_pos('a type alias can not refer to itself: $name', decl_pos.extend(type_alias_pos))
return ast.AliasTypeDecl{}
}

View File

@ -0,0 +1,3 @@
interface Foo {
bar string | int
}

View File

@ -0,0 +1,5 @@
vlib/v/parser/tests/anon_sum_type_multi_return_err.vv:1:24: error: invalid expression: unexpected token `|`
1 | fn abc() (string, int) | string {
| ^
2 | return ''
3 | }

View File

@ -0,0 +1,3 @@
fn abc() (string, int) | string {
return ''
}

View File

@ -0,0 +1,5 @@
vlib/v/parser/tests/anon_sum_type_receiver_err.vv:1:11: error: unexpected token `|`, expecting `)`
1 | fn (x int | string) baz() {
| ^
2 | println('baz')
3 | }

View File

@ -0,0 +1,3 @@
fn (x int | string) baz() {
println('baz')
}

View File

@ -0,0 +1,3 @@
struct Foo {
bar string | int
}

View File

@ -0,0 +1,5 @@
vlib/v/parser/tests/inline_sum_type_optional_err.vv:1:10: error: an inline sum type cannot be optional
1 | fn foo() ?string | int {
| ~~~~~~~~~~~~~
2 | return 0
3 | }

View File

@ -0,0 +1,3 @@
fn foo() ?string | int {
return 0
}

View File

@ -0,0 +1,21 @@
vlib/v/parser/tests/inline_sum_type_return_type_too_many_variants.vv:4:6: warning: an inline sum type expects a maximum of 3 types (5 were given)
2 |
3 | struct Foo {
4 | bar int | string | token.Position | bool | u32
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 | }
6 |
vlib/v/parser/tests/inline_sum_type_return_type_too_many_variants.vv:7:12: warning: an inline sum type expects a maximum of 3 types (5 were given)
5 | }
6 |
7 | fn foo(arg int | string | token.Position | bool | u32) int | string | token.Position | bool | u32 {
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
8 | return 1
9 | }
vlib/v/parser/tests/inline_sum_type_return_type_too_many_variants.vv:7:56: warning: an inline sum type expects a maximum of 3 types (5 were given)
5 | }
6 |
7 | fn foo(arg int | string | token.Position | bool | u32) int | string | token.Position | bool | u32 {
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
8 | return 1
9 | }

View File

@ -0,0 +1,9 @@
import v.token
struct Foo {
bar int | string | token.Position | bool | u32
}
fn foo(arg int | string | token.Position | bool | u32) int | string | token.Position | bool | u32 {
return 1
}

View File

@ -0,0 +1,3 @@
vlib/v/parser/tests/named_sum_type_none_err.vv:1:34: error: named sum type cannot have none as its variant
1 | type Abc = string | int | bool | none
| ~~~~

View File

@ -0,0 +1 @@
type Abc = string | int | bool | none

View File

@ -0,0 +1,5 @@
vlib/v/parser/tests/option_sum_type_return_err.vv:1:21: error: an inline sum type cannot be optional
1 | fn option_sumtype() ?string | int {
| ~~~~~~~~~~~~~
2 | return 0
3 | }

View File

@ -0,0 +1,3 @@
fn option_sumtype() ?string | int {
return 0
}

View File

@ -0,0 +1,41 @@
fn returns_sumtype() int | string {
return 1
}
fn returns_sumtype_reverse() int | string {
return 1
}
fn test_stringification() {
x := returns_sumtype()
y := returns_sumtype_reverse()
assert '$x' == '$y'
}
struct Milk {
egg int | string
}
fn test_struct_with_inline_sumtype() {
m := Milk{
egg: 1
}
assert m.egg is int
}
interface IMilk {
egg int | string
}
fn receive_imilk(milk IMilk) {}
fn test_interface_with_inline_sumtype() {
m := Milk{
egg: 1
}
receive_imilk(m)
}
fn returns_sumtype_in_multireturn() (int | string, string) {
return 1, ''
}

View File

@ -1,5 +0,0 @@
type MapValue = int | none
fn test_sum_type_with_none_type() {
assert true
}