diff --git a/doc/docs.md b/doc/docs.md index 99aa3bbbcb6959..9992711193f812 100644 --- a/doc/docs.md +++ b/doc/docs.md @@ -78,6 +78,7 @@ by using any of the following commands in a terminal: * [Array slices](#array-slices) * [Fixed size arrays](#fixed-size-arrays) * [Maps](#maps) + * [Map update syntax](#map-update-syntax) @@ -480,7 +481,7 @@ fn main() { } } ``` -While variable shadowing is not allowed, field shadowing is allowed. +While variable shadowing is not allowed, field shadowing is allowed. ```v pub struct Dimension { width int = -1 @@ -1462,6 +1463,36 @@ See all methods of and [maps](https://modules.vlang.io/maps.html). +### Map update syntax + +As with stucts, V lets you initialise a map with an update applied on top of +another map: + +```v +const base_map = { + 'a': 4 + 'b': 5 +} + +foo := { + ...base_map + 'b': 88 + 'c': 99 +} + +println(foo) // {'a': 4, 'b': 88, 'c': 99} +``` + +This is functionally equivalent to cloning the map and updating it, except that +you don't have to declare a mutable variable: + +```v failcompile +// same as above (except mutable) +mut foo := base_map.clone() +foo['b'] = 88 +foo['c'] = 99 +``` + ## Module imports For information about creating a module, see [Modules](#modules). @@ -5634,7 +5665,7 @@ serializers for any data format. V has compile time `if` and `for` constructs: ####

.fields

-You can iterate over struct fields using `.fields`, it also works with generic types +You can iterate over struct fields using `.fields`, it also works with generic types (e.g. `T.fields`) and generic arguments (e.g. `param.fields` where `fn gen[T](param T) {`). ```v @@ -7325,4 +7356,4 @@ Assignment Operators += -= *= /= %= &= |= ^= >>= <<= >>>= -``` \ No newline at end of file +``` diff --git a/vlib/builtin/map.v b/vlib/builtin/map.v index d5daae6280ffa3..5fb8c4bcf2a028 100644 --- a/vlib/builtin/map.v +++ b/vlib/builtin/map.v @@ -284,6 +284,20 @@ fn new_map_init(hash_fn MapHashFn, key_eq_fn MapEqFn, clone_fn MapCloneFn, free_ return out } +fn new_map_update_init(update &map, n int, key_bytes int, value_bytes int, keys voidptr, values voidptr) map { + mut out := unsafe { update.clone() } + mut pkey := &u8(keys) + mut pval := &u8(values) + for _ in 0 .. n { + unsafe { + out.set(pkey, pval) + pkey = pkey + key_bytes + pval = pval + value_bytes + } + } + return out +} + // move moves the map to a new location in memory. // It does this by copying to a new location, then setting the // old location to all `0` with `vmemset` diff --git a/vlib/v/ast/ast.v b/vlib/v/ast/ast.v index e744e72762729a..a9b80b84fe75ae 100644 --- a/vlib/v/ast/ast.v +++ b/vlib/v/ast/ast.v @@ -1502,12 +1502,16 @@ pub: comments [][]Comment // comments after key-value pairs pre_cmnts []Comment // comments before the first key-value pair pub mut: - keys []Expr - vals []Expr - val_types []Type - typ Type - key_type Type - value_type Type + keys []Expr + vals []Expr + val_types []Type + typ Type + key_type Type + value_type Type + has_update_expr bool // has `...a` + update_expr Expr // `a` in `...a` + update_expr_pos token.Pos + update_expr_comments []Comment } // s[10..20] diff --git a/vlib/v/checker/containers.v b/vlib/v/checker/containers.v index f3aa960a2360d0..dc106ca22cbe6c 100644 --- a/vlib/v/checker/containers.v +++ b/vlib/v/checker/containers.v @@ -407,7 +407,7 @@ fn (mut c Checker) array_fixed_has_unresolved_size(info &ast.ArrayFixed) bool { fn (mut c Checker) map_init(mut node ast.MapInit) ast.Type { // `map = {}` - if node.keys.len == 0 && node.vals.len == 0 && node.typ == 0 { + if node.keys.len == 0 && node.vals.len == 0 && !node.has_update_expr && node.typ == 0 { sym := c.table.sym(c.expected_type) if sym.kind == .map { info := sym.map_info() @@ -455,90 +455,107 @@ fn (mut c Checker) map_init(mut node ast.MapInit) ast.Type { return node.typ } - if node.keys.len > 0 && node.vals.len > 0 { - mut key0_type := ast.void_type - mut val0_type := ast.void_type + if (node.keys.len > 0 && node.vals.len > 0) || node.has_update_expr { + mut map_type := ast.void_type use_expected_type := c.expected_type != ast.void_type && !c.inside_const && c.table.sym(c.expected_type).kind == .map && !(c.inside_fn_arg && c.expected_type.has_flag(.generic)) if use_expected_type { - sym := c.table.sym(c.expected_type) + map_type = c.expected_type + } + if node.has_update_expr { + update_type := c.expr(mut node.update_expr) + if map_type != ast.void_type { + if update_type != map_type { + msg := c.expected_msg(update_type, map_type) + c.error('invalid map update: ${msg}', node.update_expr_pos) + } + } else if c.table.sym(update_type).kind != .map { + c.error('invalid map update: non-map type', node.update_expr_pos) + } else { + map_type = update_type + } + } + + mut map_key_type := ast.void_type + mut map_val_type := ast.void_type + if map_type != ast.void_type { + sym := c.table.sym(map_type) info := sym.map_info() - key0_type = c.unwrap_generic(info.key_type) - val0_type = c.unwrap_generic(info.value_type) - } else { + map_key_type = info.key_type + map_val_type = info.value_type + } else if node.keys.len > 0 { // `{'age': 20}` mut key_ := node.keys[0] - key0_type = ast.mktyp(c.expr(mut key_)) + map_key_type = ast.mktyp(c.expr(mut key_)) if node.keys[0].is_auto_deref_var() { - key0_type = key0_type.deref() + map_key_type = map_key_type.deref() } mut val_ := node.vals[0] - val0_type = ast.mktyp(c.expr(mut val_)) + map_val_type = ast.mktyp(c.expr(mut val_)) if node.vals[0].is_auto_deref_var() { - val0_type = val0_type.deref() + map_val_type = map_val_type.deref() + } + node.val_types << map_val_type + if node.keys.len == 1 && map_val_type == ast.none_type { + c.error('map value cannot be only `none`', node.vals[0].pos()) } - node.val_types << val0_type } - key0_type = c.unwrap_generic(key0_type) - val0_type = c.unwrap_generic(val0_type) - map_type := ast.new_type(c.table.find_or_register_map(key0_type, val0_type)) - node.typ = map_type - node.key_type = key0_type - node.value_type = val0_type - map_value_sym := c.table.sym(node.value_type) - expecting_interface_map := map_value_sym.kind == .interface_ - // - mut same_key_type := true + map_key_type = c.unwrap_generic(map_key_type) + map_val_type = c.unwrap_generic(map_val_type) - if node.keys.len == 1 && val0_type == ast.none_type { - c.error('map value cannot be only `none`', node.vals[0].pos()) - } + node.typ = ast.new_type(c.table.find_or_register_map(map_key_type, map_val_type)) + node.key_type = map_key_type + node.value_type = map_val_type + map_value_sym := c.table.sym(map_val_type) + expecting_interface_map := map_value_sym.kind == .interface_ + mut same_key_type := true for i, mut key in node.keys { - if i == 0 && !use_expected_type { - continue + if i == 0 && map_type == ast.void_type { + continue // skip first key/value if we processed them above } mut val := node.vals[i] - c.expected_type = key0_type + c.expected_type = map_key_type key_type := c.expr(mut key) - c.expected_type = val0_type + c.expected_type = map_val_type val_type := c.expr(mut val) node.val_types << val_type val_type_sym := c.table.sym(val_type) - if !c.check_types(key_type, key0_type) || (i == 0 && key_type.is_number() - && key0_type.is_number() && key0_type != ast.mktyp(key_type)) { - msg := c.expected_msg(key_type, key0_type) + if !c.check_types(key_type, map_key_type) + || (i == 0 && key_type.is_number() && map_key_type.is_number() + && map_key_type != ast.mktyp(key_type)) { + msg := c.expected_msg(key_type, map_key_type) c.error('invalid map key: ${msg}', key.pos()) same_key_type = false } if expecting_interface_map { - if val_type == node.value_type { + if val_type == map_val_type { continue } if val_type_sym.kind == .struct_ - && c.type_implements(val_type, node.value_type, val.pos()) { + && c.type_implements(val_type, map_val_type, val.pos()) { node.vals[i] = ast.CastExpr{ expr: val - typname: c.table.get_type_name(node.value_type) - typ: node.value_type + typname: c.table.get_type_name(map_val_type) + typ: map_val_type expr_type: val_type pos: val.pos() } continue } else { - msg := c.expected_msg(val_type, node.value_type) + msg := c.expected_msg(val_type, map_val_type) c.error('invalid map value: ${msg}', val.pos()) } } - if val_type == ast.none_type && val0_type.has_flag(.option) { + if val_type == ast.none_type && map_val_type.has_flag(.option) { continue } - if !c.check_types(val_type, val0_type) - || val0_type.has_flag(.option) != val_type.has_flag(.option) - || (i == 0 && val_type.is_number() && val0_type.is_number() - && val0_type != ast.mktyp(val_type)) { - msg := c.expected_msg(val_type, val0_type) + if !c.check_types(val_type, map_val_type) + || map_val_type.has_flag(.option) != val_type.has_flag(.option) + || (i == 0 && val_type.is_number() && map_val_type.is_number() + && map_val_type != ast.mktyp(val_type)) { + msg := c.expected_msg(val_type, map_val_type) c.error('invalid map value: ${msg}', val.pos()) } } @@ -547,7 +564,6 @@ fn (mut c Checker) map_init(mut node ast.MapInit) ast.Type { c.check_dup_keys(node, i) } } - return map_type } return node.typ } diff --git a/vlib/v/checker/tests/map_init_invalid_update.out b/vlib/v/checker/tests/map_init_invalid_update.out new file mode 100644 index 00000000000000..08d20b6f3d06dc --- /dev/null +++ b/vlib/v/checker/tests/map_init_invalid_update.out @@ -0,0 +1,14 @@ +vlib/v/checker/tests/map_init_invalid_update.vv:8:6: error: invalid map update: non-map type + 6 | foo := 'foo is a string' + 7 | a := { + 8 | ...foo // not a map + | ~~~ + 9 | 'a': 5 + 10 | 'b': 6 +vlib/v/checker/tests/map_init_invalid_update.vv:19:6: error: invalid map update: non-map type + 17 | println(b) + 18 | c := { + 19 | ...Foo{9} // also not ok + | ~~~~~~ + 20 | } + 21 | println(c) diff --git a/vlib/v/checker/tests/map_init_invalid_update.vv b/vlib/v/checker/tests/map_init_invalid_update.vv new file mode 100644 index 00000000000000..8bf148fc84d8fd --- /dev/null +++ b/vlib/v/checker/tests/map_init_invalid_update.vv @@ -0,0 +1,22 @@ +struct Foo { + x int +} + +fn main() { + foo := 'foo is a string' + a := { + ...foo // not a map + 'a': 5 + 'b': 6 + } + println(a) + b := { + ...(a.clone()) // ok + 'c': 99 + } + println(b) + c := { + ...Foo{9} // also not ok + } + println(c) +} diff --git a/vlib/v/checker/tests/map_init_wrong_expected_type.out b/vlib/v/checker/tests/map_init_wrong_expected_type.out new file mode 100644 index 00000000000000..1ad16d8431cb55 --- /dev/null +++ b/vlib/v/checker/tests/map_init_wrong_expected_type.out @@ -0,0 +1,49 @@ +vlib/v/checker/tests/map_init_wrong_expected_type.vv:18:9: error: invalid map value: expected `int`, not `string` + 16 | b := Bar{ + 17 | m: { + 18 | 'a': '5' + | ~~~ + 19 | 'b': '6' + 20 | } // bad +vlib/v/checker/tests/map_init_wrong_expected_type.vv:19:9: error: invalid map value: expected `int`, not `string` + 17 | m: { + 18 | 'a': '5' + 19 | 'b': '6' + | ~~~ + 20 | } // bad + 21 | } +vlib/v/checker/tests/map_init_wrong_expected_type.vv:36:9: error: invalid map value: expected `int`, not `string` + 34 | m: { + 35 | ...a.m + 36 | 'c': '7' + | ~~~ + 37 | } // bad values + 38 | } +vlib/v/checker/tests/map_init_wrong_expected_type.vv:43:9: error: invalid map update: expected `map[string]int`, not `map[string]string` + 41 | f := Bar{ + 42 | m: { + 43 | ...x.m + | ^ + 44 | } // bad update + 45 | } +vlib/v/checker/tests/map_init_wrong_expected_type.vv:48:9: error: invalid map update: expected `map[string]int`, not `map[string]string` + 46 | g := Bar{ + 47 | m: { + 48 | ...x.m + | ^ + 49 | 'c': 7 + 50 | } // bad update, ok values +vlib/v/checker/tests/map_init_wrong_expected_type.vv:54:9: error: invalid map update: expected `map[string]int`, not `map[string]string` + 52 | h := Bar{ + 53 | m: { + 54 | ...x.m + | ^ + 55 | 'c': '7' + 56 | } // bad update, bad values +vlib/v/checker/tests/map_init_wrong_expected_type.vv:55:9: error: invalid map value: expected `int`, not `string` + 53 | m: { + 54 | ...x.m + 55 | 'c': '7' + | ~~~ + 56 | } // bad update, bad values + 57 | } diff --git a/vlib/v/checker/tests/map_init_wrong_expected_type.vv b/vlib/v/checker/tests/map_init_wrong_expected_type.vv new file mode 100644 index 00000000000000..ee6b0a06df72a8 --- /dev/null +++ b/vlib/v/checker/tests/map_init_wrong_expected_type.vv @@ -0,0 +1,66 @@ +struct Foo { + m map[string]string +} + +struct Bar { + m map[string]int +} + +fn main() { + a := Bar{ + m: { + 'a': 5 + 'b': 6 + } // ok + } + b := Bar{ + m: { + 'a': '5' + 'b': '6' + } // bad + } + c := Bar{ + m: { + ...a.m + } // ok + } + d := Bar{ + m: { + ...a.m + 'c': 7 + } // ok + } + e := Bar{ + m: { + ...a.m + 'c': '7' + } // bad values + } + + x := Foo{} + f := Bar{ + m: { + ...x.m + } // bad update + } + g := Bar{ + m: { + ...x.m + 'c': 7 + } // bad update, ok values + } + h := Bar{ + m: { + ...x.m + 'c': '7' + } // bad update, bad values + } + println(a) + println(b) + println(c) + println(d) + println(e) + println(f) + println(g) + println(h) +} diff --git a/vlib/v/checker/tests/map_init_wrong_update_type.out b/vlib/v/checker/tests/map_init_wrong_update_type.out new file mode 100644 index 00000000000000..57197267e6fd04 --- /dev/null +++ b/vlib/v/checker/tests/map_init_wrong_update_type.out @@ -0,0 +1,28 @@ +vlib/v/checker/tests/map_init_wrong_update_type.vv:12:8: error: invalid map value: expected `StrB`, not `int literal` + 10 | a := { + 11 | ...foo // not ok + 12 | 'a': 5 + | ^ + 13 | 'b': 6 + 14 | } +vlib/v/checker/tests/map_init_wrong_update_type.vv:13:8: error: invalid map value: expected `StrB`, not `int literal` + 11 | ...foo // not ok + 12 | 'a': 5 + 13 | 'b': 6 + | ^ + 14 | } + 15 | b := { +vlib/v/checker/tests/map_init_wrong_update_type.vv:17:3: error: invalid map key: expected `StrA`, not `Bar` + 15 | b := { + 16 | ...foo // not ok + 17 | Bar{'yes'}: '5' + | ~~~~~~~~~~ + 18 | Bar{'now'}: '6' + 19 | } +vlib/v/checker/tests/map_init_wrong_update_type.vv:18:3: error: invalid map key: expected `StrA`, not `Bar` + 16 | ...foo // not ok + 17 | Bar{'yes'}: '5' + 18 | Bar{'now'}: '6' + | ~~~~~~~~~~ + 19 | } + 20 | c := { diff --git a/vlib/v/checker/tests/map_init_wrong_update_type.vv b/vlib/v/checker/tests/map_init_wrong_update_type.vv new file mode 100644 index 00000000000000..618028b7670964 --- /dev/null +++ b/vlib/v/checker/tests/map_init_wrong_update_type.vv @@ -0,0 +1,28 @@ +struct Bar { + x string +} + +type StrA = string +type StrB = string + +fn main() { + foo := map[StrA]StrB{} + a := { + ...foo // not ok + 'a': 5 + 'b': 6 + } + b := { + ...foo // not ok + Bar{'yes'}: '5' + Bar{'now'}: '6' + } + c := { + ...foo // ok + 'up': 'down' + 'left': 'right' + } + println(a) + println(b) + println(c) +} diff --git a/vlib/v/fmt/fmt.v b/vlib/v/fmt/fmt.v index e37baad3098eff..2f450b329c2703 100644 --- a/vlib/v/fmt/fmt.v +++ b/vlib/v/fmt/fmt.v @@ -2624,7 +2624,7 @@ pub fn (mut f Fmt) lock_expr(node ast.LockExpr) { } pub fn (mut f Fmt) map_init(node ast.MapInit) { - if node.keys.len == 0 { + if node.keys.len == 0 && !node.has_update_expr { if node.typ > ast.void_type { sym := f.table.sym(node.typ) info := sym.info as ast.Map @@ -2644,6 +2644,15 @@ pub fn (mut f Fmt) map_init(node ast.MapInit) { f.writeln('{') f.indent++ f.comments(node.pre_cmnts) + if node.has_update_expr { + f.write('...') + f.expr(node.update_expr) + f.comments(node.update_expr_comments, + prev_line: node.update_expr_pos.last_line + has_nl: false + ) + f.writeln('') + } mut max_field_len := 0 mut skeys := []string{} for key in node.keys { diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index 6a238a478ebce7..2e6a8888aad0e3 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -4136,25 +4136,25 @@ fn (mut g Gen) map_init(node ast.MapInit) { } } if size > 0 { - if value_sym.kind == .function { - g.writeln('new_map_init${noscan}(${hash_fn}, ${key_eq_fn}, ${clone_fn}, ${free_fn}, ${size}, sizeof(${key_typ_str}), sizeof(voidptr),') + effective_typ_str := if value_sym.kind == .function { 'voidptr' } else { value_typ_str } + if node.has_update_expr { + g.writeln('new_map_update_init(') + g.write('\t&(') + g.expr(node.update_expr) + g.writeln('), ${size}, sizeof(${key_typ_str}), sizeof(${effective_typ_str}),') } else { - g.writeln('new_map_init${noscan}(${hash_fn}, ${key_eq_fn}, ${clone_fn}, ${free_fn}, ${size}, sizeof(${key_typ_str}), sizeof(${value_typ_str}),') + g.writeln('new_map_init${noscan}(${hash_fn}, ${key_eq_fn}, ${clone_fn}, ${free_fn}, ${size}, sizeof(${key_typ_str}), sizeof(${effective_typ_str}),') } - g.writeln('\t\t_MOV((${key_typ_str}[${size}]){') + g.writeln('\t_MOV((${key_typ_str}[${size}]){') for expr in node.keys { - g.write('\t\t\t') + g.write('\t\t') g.expr(expr) - g.writeln(', ') - } - g.writeln('\t\t}),') - if value_sym.kind == .function { - g.writeln('\t\t_MOV((voidptr[${size}]){') - } else { - g.writeln('\t\t_MOV((${value_typ_str}[${size}]){') + g.writeln(',') } + g.writeln('\t}),') + g.writeln('\t_MOV((${effective_typ_str}[${size}]){') for i, expr in node.vals { - g.write('\t\t\t') + g.write('\t\t') if expr.is_auto_deref_var() { g.write('*') } @@ -4167,12 +4167,15 @@ fn (mut g Gen) map_init(node ast.MapInit) { } g.writeln(', ') } - g.writeln('\t\t})') - g.writeln('\t)') + g.writeln('\t})') + g.writeln(')') + } else if node.has_update_expr { + g.write('map_clone(&(') + g.expr(node.update_expr) + g.writeln('))') } else { - g.write('new_map${noscan}(sizeof(${key_typ_str}), sizeof(${value_typ_str}), ${hash_fn}, ${key_eq_fn}, ${clone_fn}, ${free_fn})') + g.writeln('new_map${noscan}(sizeof(${key_typ_str}), sizeof(${value_typ_str}), ${hash_fn}, ${key_eq_fn}, ${clone_fn}, ${free_fn})') } - g.writeln('') if g.is_shared { g.write('}, sizeof(${shared_styp}))') } else if is_amp { diff --git a/vlib/v/parser/containers.v b/vlib/v/parser/containers.v index 8d6a4f7be5f008..69ac305e029a90 100644 --- a/vlib/v/parser/containers.v +++ b/vlib/v/parser/containers.v @@ -196,7 +196,22 @@ fn (mut p Parser) map_init() ast.MapInit { mut keys := []ast.Expr{} mut vals := []ast.Expr{} mut comments := [][]ast.Comment{} + mut has_update_expr := false + mut update_expr := ast.empty_expr + mut update_expr_comments := []ast.Comment{} + mut update_expr_pos := token.Pos{} pre_cmnts := p.eat_comments() + if p.tok.kind == .ellipsis { + // updating init { ...base_map, 'b': 44, 'c': 55 } + has_update_expr = true + p.check(.ellipsis) + update_expr = p.expr(0) + update_expr_pos = update_expr.pos() + if p.tok.kind == .comma { + p.next() + } + update_expr_comments << p.eat_comments(same_line: true) + } for p.tok.kind !in [.rcbr, .eof] { if p.tok.kind == .name && p.tok.lit in ['r', 'c', 'js'] { key := p.string_expr() @@ -219,6 +234,10 @@ fn (mut p Parser) map_init() ast.MapInit { pos: first_pos.extend_with_last_line(p.tok.pos(), p.tok.line_nr) comments: comments pre_cmnts: pre_cmnts + has_update_expr: has_update_expr + update_expr: update_expr + update_expr_pos: update_expr_pos + update_expr_comments: update_expr_comments } } diff --git a/vlib/v/tests/map_init_with_update_test.v b/vlib/v/tests/map_init_with_update_test.v new file mode 100644 index 00000000000000..76a350cfac0a07 --- /dev/null +++ b/vlib/v/tests/map_init_with_update_test.v @@ -0,0 +1,52 @@ +const base_map = { + 'a': 4 + 'b': 5 +} + +fn test_map_init_with_update() { + foo := { + ...base_map + 'b': 88 + 'c': 99 + } + assert base_map.keys() == ['a', 'b'] + assert base_map['a'] == 4 + assert base_map['b'] == 5 + assert foo.keys() == ['a', 'b', 'c'] + assert foo['a'] == 4 + assert foo['b'] == 88 + assert foo['c'] == 99 + + bar := { + ...foo + 'b': 6 + 'd': 7 + } + assert base_map.keys() == ['a', 'b'] + assert base_map['a'] == 4 + assert base_map['b'] == 5 + assert foo.keys() == ['a', 'b', 'c'] + assert foo['a'] == 4 + assert foo['b'] == 88 + assert foo['c'] == 99 + assert bar.keys() == ['a', 'b', 'c', 'd'] + assert bar['a'] == 4 + assert bar['b'] == 6 + assert bar['c'] == 99 + assert bar['d'] == 7 +} + +fn test_map_init_with_only_update() { + mut foo := { + ...base_map + } + bar := { + ...foo + } + foo['a'] = 99 + foo['c'] = 99 + assert bar.keys() == ['a', 'b'] + assert bar['a'] == 4 + assert bar['b'] == 5 + assert bar == base_map +}