From 16e9dff060e9e7db1a6fdd8d171127734d6a6849 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Mon, 25 May 2026 22:13:43 +0300 Subject: [PATCH] v/v2: support array update/spread syntax `[...base, e1, e2]` (#22834) (#27261) --- doc/docs.md | 22 ++++ vlib/builtin/array.v | 13 ++ vlib/v/ast/ast.v | 32 ++--- vlib/v/ast/str.v | 5 + vlib/v/checker/containers.v | 33 +++++ vlib/v/fmt/fmt.v | 20 +++ vlib/v/fmt/tests/array_update_comment_keep.vv | 8 ++ vlib/v/gen/c/array.v | 44 +++++++ vlib/v/markused/walker.v | 10 ++ vlib/v/parser/containers.v | 63 +++++++--- .../array_init_with_spread_test.v | 114 ++++++++++++++++++ vlib/v/transformer/array.v | 6 +- vlib/v2/ast/ast.v | 13 +- vlib/v2/ast_dump/ast_dump.v | 5 + vlib/v2/eval/eval.v | 17 +++ vlib/v2/gen/cleanc/array.v | 1 + vlib/v2/gen/cleanc/fn.v | 6 + vlib/v2/gen/v/gen.v | 10 +- vlib/v2/markused/markused.v | 6 + vlib/v2/parser/parser.v | 19 +++ vlib/v2/transformer/struct.v | 102 +++++++++++++++- vlib/v2/transformer/types.v | 13 +- vlib/v2/types/checker.v | 28 +++++ 23 files changed, 536 insertions(+), 54 deletions(-) create mode 100644 vlib/v/fmt/tests/array_update_comment_keep.vv create mode 100644 vlib/v/tests/builtin_arrays/array_init_with_spread_test.v diff --git a/doc/docs.md b/doc/docs.md index 9abc65f2e..a1807032f 100644 --- a/doc/docs.md +++ b/doc/docs.md @@ -109,6 +109,7 @@ The `v new --web` template uses `veb`, V's web framework. * [Runes](#runes) * [Numbers](#numbers) * [Arrays](#arrays) + * [Array update syntax](#array-update-syntax) * [Multidimensional arrays](#multidimensional-arrays) * [Array methods](#array-methods) * [Array slices](#array-slices) @@ -1103,6 +1104,27 @@ mut square := []int{len: 6, init: index * index} // square == [0, 1, 4, 9, 16, 25] ``` +#### Array update syntax + +V lets you initialise an array by spreading an existing array, optionally +followed by additional elements: + +```v +base := [1, 2] +a := [...base, 3, 4] +assert a == [1, 2, 3, 4] + +// Append to a copy of `base`, leaving `base` itself unchanged: +mut b := [...base] +b << 99 +assert base == [1, 2] +assert b == [1, 2, 99] +``` + +This is functionally equivalent to cloning the array and appending to it, +except that you don't have to declare a mutable variable for the intermediate +value, and the additional elements may be inlined at the call site. + #### Array Types An array can be of these types: diff --git a/vlib/builtin/array.v b/vlib/builtin/array.v index 8de7cf82c..3b9e0e4d4 100644 --- a/vlib/builtin/array.v +++ b/vlib/builtin/array.v @@ -333,6 +333,19 @@ fn new_array_from_c_array(len int, cap int, elm_size int, c_array voidptr) array return arr } +// Private function, used by V (`merged := [...base, 4, 5]`). +// `base` is already an independent (typed/deep) clone produced by the caller +// (cgen emits `array_clone_static_to_depth(base, depth)`), so this helper just +// appends `new_count` additional elements stored contiguously at `c_array`. +fn new_array_from_array_and_c_array(base array, new_count int, elm_size int, c_array voidptr) array { + panic_on_negative_len(new_count) + mut arr := base + if new_count > 0 && c_array != unsafe { nil } { + unsafe { arr.push_many(c_array, new_count) } + } + return arr +} + // Private function, used by V (`nums := [1, 2, 3] !`) fn new_array_from_c_array_no_alloc(len int, cap int, elm_size int, c_array voidptr) array { panic_on_negative_len(len) diff --git a/vlib/v/ast/ast.v b/vlib/v/ast/ast.v index 4b34c4969..510385067 100644 --- a/vlib/v/ast/ast.v +++ b/vlib/v/ast/ast.v @@ -1739,20 +1739,24 @@ pub: has_init bool has_index bool // true if temp variable index is used pub mut: - exprs []Expr // `[expr, expr]` or `[expr]Type{}` for fixed array - len_expr Expr // len: expr - cap_expr Expr // cap: expr - init_expr Expr // init: expr - elem_type_expr Expr = empty_expr // `typeof(expr).idx` in `[]typeof(expr).idx{}` - expr_types []Type // [Dog, Cat] // also used for interface_types - elem_type Type // element type - generic_elem_type Type // original generic element type; reused for later concrete instantiations - init_type Type // init: value type - typ Type // array type - literal_typ Type // array type as written, preserved for fmt - generic_typ Type // original generic array type; reused for later concrete instantiations - alias_type Type // alias type - has_callexpr bool // has expr which needs tmp var to initialize it + exprs []Expr // `[expr, expr]` or `[expr]Type{}` for fixed array + len_expr Expr // len: expr + cap_expr Expr // cap: expr + init_expr Expr // init: expr + elem_type_expr Expr = empty_expr // `typeof(expr).idx` in `[]typeof(expr).idx{}` + expr_types []Type // [Dog, Cat] // also used for interface_types + elem_type Type // element type + generic_elem_type Type // original generic element type; reused for later concrete instantiations + init_type Type // init: value type + typ Type // array type + literal_typ Type // array type as written, preserved for fmt + generic_typ Type // original generic array type; reused for later concrete instantiations + alias_type Type // alias type + has_callexpr bool // has expr which needs tmp var to initialize it + has_update_expr bool // has `...a` as in `[...a, 3, 4]` + update_expr Expr // `a` in `...a` + update_expr_pos token.Pos + update_expr_comments []Comment } pub struct ArrayDecompose { diff --git a/vlib/v/ast/str.v b/vlib/v/ast/str.v index 368094d3e..cfc56282e 100644 --- a/vlib/v/ast/str.v +++ b/vlib/v/ast/str.v @@ -493,6 +493,11 @@ pub fn (x Expr) str() string { return '${x.exprs.str()}${typ_str}{}' } else if x.exprs.len == 0 && typ_str != '' { return '[]${typ_str}{}' + } else if x.has_update_expr { + if x.exprs.len == 0 { + return '[...${x.update_expr}]' + } + return '[...${x.update_expr}, ${x.exprs.map(it.str()).join(', ')}]' } else { return x.exprs.str() } diff --git a/vlib/v/checker/containers.v b/vlib/v/checker/containers.v index 2a4f7b7cd..c052835bc 100644 --- a/vlib/v/checker/containers.v +++ b/vlib/v/checker/containers.v @@ -408,6 +408,39 @@ fn (mut c Checker) array_init(mut node ast.ArrayInit) ast.Type { return array_init_result_type(node) } + if node.has_update_expr { + // `[...base, e1, e2]` — array update/spread literal + update_typ := c.expr(mut node.update_expr) + // Resolve through type aliases so `type Ints = []int; [...Ints(...)]` + // is accepted; use final_sym to look past aliases of arrays. + update_sym := c.table.final_sym(update_typ) + if update_sym.kind != .array { + c.error('invalid array update: non-array type `${c.table.type_to_str(update_typ)}`', + node.update_expr_pos) + return ast.void_type + } + array_info := update_sym.array_info() + node.elem_type = array_info.elem_type + node.typ = update_typ + elem_type = array_info.elem_type + c.expected_type = elem_type + for mut expr in node.exprs { + typ := c.check_expr_option_or_result_call(expr, c.expr(mut expr)) + node.expr_types << typ + if expr is ast.CallExpr { + ret_sym := c.table.sym(typ) + if ret_sym.kind == .array_fixed { + node.expr_types[node.expr_types.len - 1] = c.cast_fixed_array_ret(typ, + ret_sym) + } + node.has_callexpr = true + } + c.check_expected(typ, elem_type) or { + c.error('invalid array element: ${err.msg()}', expr.pos()) + } + } + return array_init_result_type(node) + } if node.is_fixed { c.ensure_type_exists(node.elem_type, node.elem_type_pos) if !c.is_builtin_mod { diff --git a/vlib/v/fmt/fmt.v b/vlib/v/fmt/fmt.v index 29dcfc65c..a347dd617 100644 --- a/vlib/v/fmt/fmt.v +++ b/vlib/v/fmt/fmt.v @@ -1989,6 +1989,26 @@ pub fn (mut f Fmt) array_init(node ast.ArrayInit) { f.writeln('') } } + if node.has_update_expr { + f.write('...') + f.expr(node.update_expr) + if node.exprs.len > 0 { + f.write(',') + } + if node.update_expr_comments.len > 0 { + f.write(' ') + f.comments(node.update_expr_comments, + prev_line: node.update_expr_pos.last_line + has_nl: false + ) + last_line_nr = node.update_expr_comments.last().pos.last_line + } else { + last_line_nr = node.update_expr_pos.last_line + } + if node.exprs.len > 0 && node.update_expr_comments.len == 0 { + f.write(' ') + } + } for i, c in node.pre_cmnts { if i < node.pre_cmnts.len - 1 { if c.pos.last_line < node.pre_cmnts[i + 1].pos.line_nr { diff --git a/vlib/v/fmt/tests/array_update_comment_keep.vv b/vlib/v/fmt/tests/array_update_comment_keep.vv new file mode 100644 index 000000000..079fd77c9 --- /dev/null +++ b/vlib/v/fmt/tests/array_update_comment_keep.vv @@ -0,0 +1,8 @@ +fn main() { + base := [1, 2] + a := [...base, // keep + 3, + 4, + ] + println(a) +} diff --git a/vlib/v/gen/c/array.v b/vlib/v/gen/c/array.v index 86cf653b8..595ea4782 100644 --- a/vlib/v/gen/c/array.v +++ b/vlib/v/gen/c/array.v @@ -188,6 +188,50 @@ fn (mut g Gen) array_init(node ast.ArrayInit, var_name string) { g.write(')') } } + } else if node.has_update_expr { + // `[...base, e1, e2]` + // Clone `base` with the proper depth so that nested arrays / strings are + // deep-copied (matches `a := b` clone semantics). The runtime helper then + // just appends the trailing elements onto the already-owned clone. + elem_styp := g.styp(resolved_elem_type.typ) + array_depth := g.get_array_depth(resolved_elem_type.typ) + is_iface_or_sumtype := elem_sym.kind in [.sum_type, .interface] + g.write('builtin__new_array_from_array_and_c_array(builtin__array_clone_static_to_depth(') + g.expr(node.update_expr) + g.write(', ${array_depth}), ${len}, sizeof(${elem_styp}), ') + if len == 0 { + g.write('((${elem_styp}*)0))') + } else { + prepared_exprs := g.prepare_array_init_exprs(node.exprs, expr_types, resolved_elem_type.typ) + g.write('_MOV((${elem_styp}[${len}]){') + for i, expr in prepared_exprs { + actual_expr := array_init_orig_expr(expr) + expr_type := if expr_types.len > i { expr_types[i] } else { resolved_elem_type.typ } + if resolved_elem_type.typ.has_flag(.option) { + g.expr_with_opt(expr, expr_type, resolved_elem_type.typ) + } else if expr_type == ast.string_type + && actual_expr !in [ast.IndexExpr, ast.CallExpr, ast.StringLiteral, ast.StringInterLiteral, ast.InfixExpr] { + if is_iface_or_sumtype { + g.expr_with_cast(expr, expr_type, resolved_elem_type.typ) + } else { + g.write('builtin__string_clone(') + g.expr(expr) + g.write(')') + } + } else { + g.expr_with_cast(expr, expr_type, resolved_elem_type.typ) + } + if i != len - 1 { + g.write(', ') + } + } + g.write('}))') + } + if g.is_shared { + g.write('}, sizeof(${shared_styp}))') + } else if is_amp { + g.write(')') + } } else if len == 0 { // `[]int{len: 6, cap:10, init:22}` g.array_init_with_fields(node, elem_type, is_amp, shared_styp, var_name) diff --git a/vlib/v/markused/walker.v b/vlib/v/markused/walker.v index aa5d63a99..64a16746e 100644 --- a/vlib/v/markused/walker.v +++ b/vlib/v/markused/walker.v @@ -113,6 +113,7 @@ mut: uses_arr_getter bool uses_arr_clone bool uses_arr_sorted bool + uses_array_update bool // has [...expr, ...] uses_type_name bool // sum_type.type_name() } @@ -927,6 +928,10 @@ fn (mut w Walker) expr(node_ ast.Expr) { w.expr(node.cap_expr) w.expr(node.init_expr) w.exprs(node.exprs) + if node.has_update_expr { + w.expr(node.update_expr) + w.uses_array_update = true + } if w.table.final_sym(node.typ).kind == .array { if !w.inside_in_op { w.uses_array = true @@ -3662,6 +3667,11 @@ fn (mut w Walker) mark_resource_dependencies() { if w.uses_map_update { w.fn_by_name('new_map_update_init') } + if w.uses_array_update { + w.fn_by_name('new_array_from_array_and_c_array') + w.uses_array = true + w.uses_arr_clone = true + } if w.uses_mem_align { w.fn_by_name('memdup_align') } diff --git a/vlib/v/parser/containers.v b/vlib/v/parser/containers.v index eb045c20c..c470ab397 100644 --- a/vlib/v/parser/containers.v +++ b/vlib/v/parser/containers.v @@ -163,6 +163,10 @@ fn (mut p Parser) array_init(is_option bool, alias_array_type ast.Type) ast.Arra mut has_init := false mut has_index := false mut init_expr := ast.empty_expr + mut has_update_expr := false + mut update_expr := ast.empty_expr + mut update_expr_pos := token.Pos{} + mut update_expr_comments := []ast.Comment{} if alias_array_type == ast.void_type { p.check(.lsbr) if p.tok.kind == .rsbr { @@ -215,6 +219,21 @@ fn (mut p Parser) array_init(is_option bool, alias_array_type ast.Type) ast.Arra p.last_enum_name = '' p.last_enum_mod = '' pre_cmnts = p.eat_comments() + if p.tok.kind == .ellipsis { + // updating init `[...base_array, 3, 4]` + has_update_expr = true + p.check(.ellipsis) + update_expr = p.expr(0) + update_expr_pos = update_expr.pos() + // Eat comments that may sit between the spread base and the + // separating comma, e.g. `[...base /* keep */, 3]`. + update_expr_comments << p.eat_comments(same_line: true) + if p.tok.kind == .comma { + p.next() + } + update_expr_comments << p.eat_comments(same_line: true) + update_expr_comments << p.eat_comments() + } for i := 0; p.tok.kind !in [.rsbr, .eof]; i++ { exprs << if p.tok.kind == .dotdot && p.peek_tok.kind == .rsbr { ast.Expr(ast.RangeExpr{ @@ -365,26 +384,30 @@ fn (mut p Parser) array_init(is_option bool, alias_array_type ast.Type) ast.Arra } pos := first_pos.extend_with_last_line(last_pos, p.prev_tok.line_nr) return ast.ArrayInit{ - is_fixed: is_fixed - has_val: has_val - mod: p.mod - elem_type: elem_type - typ: array_type - alias_type: alias_array_type - exprs: exprs - ecmnts: ecmnts - pre_cmnts: pre_cmnts - elem_type_expr: elem_type_expr - pos: pos - elem_type_pos: elem_type_pos - has_len: has_len - len_expr: len_expr - has_cap: has_cap - has_init: has_init - has_index: has_index - cap_expr: cap_expr - init_expr: init_expr - is_option: is_option + is_fixed: is_fixed + has_val: has_val + mod: p.mod + elem_type: elem_type + typ: array_type + alias_type: alias_array_type + exprs: exprs + ecmnts: ecmnts + pre_cmnts: pre_cmnts + elem_type_expr: elem_type_expr + pos: pos + elem_type_pos: elem_type_pos + has_len: has_len + len_expr: len_expr + has_cap: has_cap + has_init: has_init + has_index: has_index + cap_expr: cap_expr + init_expr: init_expr + is_option: is_option + 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/builtin_arrays/array_init_with_spread_test.v b/vlib/v/tests/builtin_arrays/array_init_with_spread_test.v new file mode 100644 index 000000000..b8e946ab7 --- /dev/null +++ b/vlib/v/tests/builtin_arrays/array_init_with_spread_test.v @@ -0,0 +1,114 @@ +const base_array = [ + 1 + 2 +] + +const base_strings = ['a', 'b'] + +struct Point { + x int + y int +} + +const base_points = [Point{1, 2}, Point{3, 4}] + +fn test_array_init_with_spread() { + complete := [ + ...base_array, + 3, + 4, + ] + assert base_array == [1, 2] + assert complete == [1, 2, 3, 4] + assert complete.len == 4 + + second := [...base_array, 5, 6, 7] + assert second == [1, 2, 5, 6, 7] +} + +fn test_array_init_with_only_spread() { + mut copy := [...base_array] + assert copy == [1, 2] + copy << 99 + // spread produces an independent copy + assert base_array == [1, 2] + assert copy == [1, 2, 99] +} + +fn test_array_spread_strings() { + merged := [...base_strings, 'c', 'd'] + assert merged == ['a', 'b', 'c', 'd'] +} + +fn test_array_spread_structs() { + pts := [...base_points, Point{5, 6}] + assert pts.len == 3 + assert pts[0] == Point{1, 2} + assert pts[1] == Point{3, 4} + assert pts[2] == Point{5, 6} +} + +fn test_array_spread_local_var() { + v_arr := [7, 8, 9] + x := [...v_arr, 100] + assert v_arr == [7, 8, 9] + assert x == [7, 8, 9, 100] +} + +fn test_array_spread_chained() { + first := [...base_array, 3] + second := [...first, 4] + assert second == [1, 2, 3, 4] +} + +// Regression: `[...base]` should produce an independent deep copy. Modifying +// an inner []int after the spread must not affect the base. +fn test_array_spread_nested_arrays_are_deep_cloned() { + mut base := [[1, 2], [3, 4]] + mut copy := [...base] + copy[0] << 99 + assert base[0] == [1, 2] + assert copy[0] == [1, 2, 99] +} + +// Regression: assigning into a nested element of the spread copy must not +// mutate the base. Catches shallow-copy regressions in any backend that +// reuses the base's inner storage. +fn test_array_spread_nested_element_assignment_isolated() { + mut base := [[1, 2]] + mut b := [...base] + b[0][0] = 9 + assert base[0][0] == 1 + assert b[0][0] == 9 +} + +// Regression: indexing a spread literal inline (`[...base, 3][0]`) must keep +// the spread base in the AST instead of dropping it during parsing. +fn test_array_spread_indexed_inline() { + assert [...base_array, 3][0] == 1 + assert [...base_array, 3][1] == 2 + assert [...base_array, 3][2] == 3 +} + +// Regression: appended string variables must be cloned, matching the +// behavior of the regular `[s1, s2]` array-literal path. +fn test_array_spread_appended_string_variable() { + base := ['a', 'b'] + mut s := 'c' + arr := [...base, s] + s = 'mutated' + assert arr == ['a', 'b', 'c'] +} + +type Ints = []int + +// Regression: type-alias of an array should be accepted as a spread base. +fn test_array_spread_alias_base() { + a := Ints([10, 20, 30]) + b := [...a, 40] + assert b.len == 4 + assert b[0] == 10 + assert b[3] == 40 + c := [...a] + assert c.len == 3 +} diff --git a/vlib/v/transformer/array.v b/vlib/v/transformer/array.v index 7684dc5bc..fc1f890c9 100644 --- a/vlib/v/transformer/array.v +++ b/vlib/v/transformer/array.v @@ -18,8 +18,12 @@ pub fn (mut t Transformer) array_init(mut node ast.ArrayInit) ast.Expr { if node.has_init { node.init_expr = t.expr(mut node.init_expr) } + if node.has_update_expr { + node.update_expr = t.expr(mut node.update_expr) + } if t.pref.backend == .js_node || !t.pref.new_transform || t.skip_array_transform - || node.is_fixed || t.inside_in || node.has_len || node.has_cap || node.exprs.len == 0 { + || node.is_fixed || t.inside_in || node.has_len || node.has_cap || node.exprs.len == 0 + || node.has_update_expr { return node } // For C and native transform into a function call `builtin__new_array_from_c_array_noscan(...)` etc diff --git a/vlib/v2/ast/ast.v b/vlib/v2/ast/ast.v index df50d284c..e72e110fa 100644 --- a/vlib/v2/ast/ast.v +++ b/vlib/v2/ast/ast.v @@ -338,12 +338,13 @@ pub enum DeferMode { // Expressions pub struct ArrayInitExpr { pub mut: - typ Expr = empty_expr - exprs []Expr - init Expr = empty_expr - cap Expr = empty_expr - len Expr = empty_expr - pos token.Pos + typ Expr = empty_expr + exprs []Expr + init Expr = empty_expr + cap Expr = empty_expr + len Expr = empty_expr + update_expr Expr = empty_expr // `a` in `[...a, 3, 4]` + pos token.Pos } pub struct AsCastExpr { diff --git a/vlib/v2/ast_dump/ast_dump.v b/vlib/v2/ast_dump/ast_dump.v index 3d76c8ea6..ca7585b32 100644 --- a/vlib/v2/ast_dump/ast_dump.v +++ b/vlib/v2/ast_dump/ast_dump.v @@ -1009,6 +1009,11 @@ fn (mut jb JsonBuilder) write_array_init_expr(expr ast.ArrayInitExpr) { jb.write_expr(expr.len) jb.sb.write_string(',\n') + jb.write_indent() + jb.sb.write_string('"update_expr": ') + jb.write_expr(expr.update_expr) + jb.sb.write_string(',\n') + jb.write_indent() jb.sb.write_string('"pos": ') jb.write_pos(expr.pos) diff --git a/vlib/v2/eval/eval.v b/vlib/v2/eval/eval.v index e8029ba5e..2bb970523 100644 --- a/vlib/v2/eval/eval.v +++ b/vlib/v2/eval/eval.v @@ -3418,6 +3418,23 @@ fn (mut e Eval) eval_expr(expr ast.Expr) !Value { } } mut values := []Value{cap: expr.exprs.len} + // Spread syntax `[...base, e1, e2]` — prepend the base array's + // values before any explicit elements. Deep-clone each item so + // the new array does not alias the base's nested storage (mirrors + // the runtime `array__clone_to_depth` semantics used by lowered + // codegen paths). + if expr.update_expr !is ast.EmptyExpr { + base_value := e.eval_expr(expr.update_expr)! + if base_value is ArrayValue { + cloned_base := e.clone_array_to_depth(base_value, 100) + for v in cloned_base.values { + values << v + } + if elem_type_name == '' { + elem_type_name = base_value.elem_type_name + } + } + } for item in expr.exprs { values << e.eval_expr(item)! } diff --git a/vlib/v2/gen/cleanc/array.v b/vlib/v2/gen/cleanc/array.v index ba8dbd731..19a3c13a6 100644 --- a/vlib/v2/gen/cleanc/array.v +++ b/vlib/v2/gen/cleanc/array.v @@ -56,6 +56,7 @@ fn should_keep_builtin_array_decl(decl ast.FnDecl) bool { '__new_array_with_map_default', 'new_array_from_c_array', 'new_array_from_c_array_no_alloc', + 'new_array_from_array_and_c_array', 'ensure_cap', 'repeat', 'repeat_to_depth', diff --git a/vlib/v2/gen/cleanc/fn.v b/vlib/v2/gen/cleanc/fn.v index ed8dcd186..34aa15bfa 100644 --- a/vlib/v2/gen/cleanc/fn.v +++ b/vlib/v2/gen/cleanc/fn.v @@ -7434,6 +7434,9 @@ fn (mut g Gen) resolve_call_name(lhs ast.Expr, _arg_count int) string { if name == 'builtin__new_array_from_c_array_noscan' { name = 'new_array_from_c_array' } + if name == 'builtin__new_array_from_array_and_c_array' { + name = 'new_array_from_array_and_c_array' + } if name == 'builtin__array_push_noscan' { name = 'array__push' } @@ -9305,6 +9308,9 @@ fn (mut g Gen) call_expr(lhs ast.Expr, args []ast.Expr) { if name == 'builtin__new_array_from_c_array_noscan' { name = 'new_array_from_c_array' } + if name == 'builtin__new_array_from_array_and_c_array' { + name = 'new_array_from_array_and_c_array' + } if name == 'builtin__array_push_noscan' { name = 'array__push' } diff --git a/vlib/v2/gen/v/gen.v b/vlib/v2/gen/v/gen.v index 5cc4073f6..b7a1099bb 100644 --- a/vlib/v2/gen/v/gen.v +++ b/vlib/v2/gen/v/gen.v @@ -395,7 +395,15 @@ fn (mut g Gen) stmt(stmt ast.Stmt) { fn (mut g Gen) expr(expr ast.Expr) { match expr { ast.ArrayInitExpr { - if expr.exprs.len > 0 { + if expr.update_expr !is ast.EmptyExpr { + g.write('[...') + g.expr(expr.update_expr) + if expr.exprs.len > 0 { + g.write(', ') + g.expr_list(expr.exprs, ', ') + } + g.write(']') + } else if expr.exprs.len > 0 { g.write('[') g.expr_list(expr.exprs, ', ') g.write(']') diff --git a/vlib/v2/markused/markused.v b/vlib/v2/markused/markused.v index 27b49a9d0..efcd8d58e 100644 --- a/vlib/v2/markused/markused.v +++ b/vlib/v2/markused/markused.v @@ -410,6 +410,7 @@ pub fn should_keep_builtin_array_decl(decl ast.FnDecl) bool { '__new_array_with_map_default', 'new_array_from_c_array', 'new_array_from_c_array_no_alloc', + 'new_array_from_array_and_c_array', 'ensure_cap', 'repeat', 'repeat_to_depth', @@ -1037,6 +1038,10 @@ fn called_fn_name_candidates(name string) []string { add_unique_string(mut out, 'new_array_from_c_array') return out } + if name == 'builtin__new_array_from_array_and_c_array' { + add_unique_string(mut out, 'new_array_from_array_and_c_array') + return out + } if name == 'builtin__array_push_noscan' { add_unique_string(mut out, 'array__push') return out @@ -1981,6 +1986,7 @@ fn (mut w Walker) walk_expr(expr ast.Expr, mod_name string) { w.walk_expr(expr.init, mod_name) w.walk_expr(expr.cap, mod_name) w.walk_expr(expr.len, mod_name) + w.walk_expr(expr.update_expr, mod_name) } ast.AsCastExpr { w.walk_expr(expr.expr, mod_name) diff --git a/vlib/v2/parser/parser.v b/vlib/v2/parser/parser.v index a9070e26c..a24ebee15 100644 --- a/vlib/v2/parser/parser.v +++ b/vlib/v2/parser/parser.v @@ -957,6 +957,15 @@ fn (mut p Parser) expr(min_bp token.BindingPower) ast.Expr { // ArrayType in CastExpr: `[]type` in `[]type(x)` set lhs to type, cast handled later pos := p.pos p.next() + // Spread syntax `[...base, e1, e2]` — parsed before regular elements. + mut update_expr := ast.empty_expr + if p.tok == .ellipsis { + p.next() + update_expr = p.expr(.lowest) + if p.tok == .comma || p.tok == .semicolon { + p.next() + } + } // exprs in first `[]` eg. (`1,2,3,4` in `[1,2,3,4]) | (`2` in `[2]int{}`) mut exprs := []ast.Expr{} for p.tok != .rsbr { @@ -1055,6 +1064,11 @@ fn (mut p Parser) expr(min_bp token.BindingPower) ast.Expr { else { lhs = array_init_expr_with_parts(ast.empty_expr, exprs, ast.empty_expr, ast.empty_expr, ast.empty_expr, pos) + if update_expr !is ast.EmptyExpr { + mut arr_init := lhs as ast.ArrayInitExpr + arr_init.update_expr = update_expr + lhs = ast.Expr(arr_init) + } for i := 1; i < exprs_arr.len; i++ { exprs2 := exprs_arr[i] if exprs2.len != 1 { @@ -1149,6 +1163,11 @@ fn (mut p Parser) expr(min_bp token.BindingPower) ast.Expr { } else { lhs = array_init_expr_with_parts(ast.empty_expr, exprs, ast.empty_expr, ast.empty_expr, ast.empty_expr, pos) + if update_expr !is ast.EmptyExpr { + mut arr_init := lhs as ast.ArrayInitExpr + arr_init.update_expr = update_expr + lhs = ast.Expr(arr_init) + } } } .key_match { diff --git a/vlib/v2/transformer/struct.v b/vlib/v2/transformer/struct.v index cc7d32cde..d560e9d24 100644 --- a/vlib/v2/transformer/struct.v +++ b/vlib/v2/transformer/struct.v @@ -401,15 +401,27 @@ fn (mut t Transformer) transform_array_init_expr(expr ast.ArrayInitExpr) ast.Exp if t.is_eval_backend() { return ast.ArrayInitExpr{ - typ: array_typ - exprs: exprs - init: if expr.init !is ast.EmptyExpr { t.transform_expr(expr.init) } else { expr.init } - cap: if expr.cap !is ast.EmptyExpr { t.transform_expr(expr.cap) } else { expr.cap } - len: if expr.len !is ast.EmptyExpr { t.transform_expr(expr.len) } else { expr.len } - pos: expr.pos + typ: array_typ + exprs: exprs + init: if expr.init !is ast.EmptyExpr { t.transform_expr(expr.init) } else { expr.init } + cap: if expr.cap !is ast.EmptyExpr { t.transform_expr(expr.cap) } else { expr.cap } + len: if expr.len !is ast.EmptyExpr { t.transform_expr(expr.len) } else { expr.len } + update_expr: if expr.update_expr !is ast.EmptyExpr { + t.transform_expr(expr.update_expr) + } else { + expr.update_expr + } + pos: expr.pos } } + // Spread syntax `[...base, e1, e2]` — lower to + // builtin__new_array_from_array_and_c_array(array__clone_to_depth(&base, depth), + // n, sizeof(elem), {e1, e2}). + if expr.update_expr !is ast.EmptyExpr { + return t.transform_array_spread_expr(expr, exprs, elem_type_expr) + } + // Dynamic array: transform to builtin__new_array_from_c_array_noscan(len, cap, sizeof(elem), values) arr_len := exprs.len @@ -868,6 +880,84 @@ fn (mut t Transformer) transform_array_init_expr(expr ast.ArrayInitExpr) ast.Exp } } +fn (mut t Transformer) transform_array_spread_expr(expr ast.ArrayInitExpr, exprs []ast.Expr, hint_elem_type_expr ast.Expr) ast.Expr { + update_expr := t.transform_expr(expr.update_expr) + // Resolve element type from the base array. + mut elem_type_expr := hint_elem_type_expr + mut clone_depth := 0 + if base_type := t.get_expr_type(expr.update_expr) { + base_unwrapped := t.unwrap_alias_and_pointer_type(base_type) + if base_unwrapped is types.Array { + if elem_type_expr is ast.EmptyExpr { + elem_type_expr = t.type_to_ast_type_expr(base_unwrapped.elem_type) + } + // Match V2 `.clone()` semantics for nested arrays: deep-clone inner + // arrays so `[...nested]` doesn't share inner storage with `nested`. + nesting := t.get_array_nesting_depth(base_type) + if nesting > 1 { + clone_depth = nesting - 1 + } + } + } + sizeof_arg := if elem_type_expr !is ast.EmptyExpr { + elem_type_expr + } else { + ast.Expr(ast.Ident{ + name: 'int' + }) + } + cloned_base := ast.Expr(ast.CallExpr{ + lhs: ast.Ident{ + name: 'array__clone_to_depth' + } + args: [ + update_expr, + ast.Expr(ast.BasicLiteral{ + kind: .number + value: '${clone_depth}' + }), + ] + pos: expr.pos + }) + new_count := exprs.len + mut data_arg := ast.Expr(ast.CastExpr{ + typ: ast.Expr(ast.Ident{ + name: 'voidptr' + }) + expr: ast.Expr(ast.BasicLiteral{ + kind: .number + value: '0' + }) + }) + if new_count > 0 { + inner_array_typ := ast.Type(ast.ArrayType{ + elem_type: sizeof_arg + }) + data_arg = ast.Expr(ast.ArrayInitExpr{ + typ: ast.Expr(inner_array_typ) + exprs: exprs + }) + } + return ast.CallExpr{ + lhs: ast.Ident{ + name: 'builtin__new_array_from_array_and_c_array' + } + args: [ + cloned_base, + ast.Expr(ast.BasicLiteral{ + kind: .number + value: '${new_count}' + }), + ast.Expr(ast.KeywordOperator{ + op: .key_sizeof + exprs: [sizeof_arg] + }), + data_arg, + ] + pos: expr.pos + } +} + fn array_expr_with_expected_type(expr ast.Expr, expected_type ast.Expr) ast.Expr { if expr is ast.ArrayInitExpr { return ast.Expr(array_init_with_expected_type(expr, expected_type)) diff --git a/vlib/v2/transformer/types.v b/vlib/v2/transformer/types.v index 5b95c3d17..240bbff12 100644 --- a/vlib/v2/transformer/types.v +++ b/vlib/v2/transformer/types.v @@ -1359,12 +1359,13 @@ fn (mut t Transformer) resolve_expr_with_expected_type(expr ast.Expr, expected t ast.ArrayInitExpr { if expr.typ is ast.EmptyExpr && (base is types.Array || base is types.ArrayFixed) { return ast.ArrayInitExpr{ - typ: t.type_to_ast_type_expr(base) - exprs: expr.exprs - init: expr.init - cap: expr.cap - len: expr.len - pos: expr.pos + typ: t.type_to_ast_type_expr(base) + exprs: expr.exprs + init: expr.init + cap: expr.cap + len: expr.len + update_expr: expr.update_expr + pos: expr.pos } } } diff --git a/vlib/v2/types/checker.v b/vlib/v2/types/checker.v index 130ae2b69..c8fc6b215 100644 --- a/vlib/v2/types/checker.v +++ b/vlib/v2/types/checker.v @@ -2513,6 +2513,34 @@ fn (mut c Checker) expr_impl(expr ast.Expr) Type { // the native stack before the general expression-depth guard can recover. // NOTE: expr.init is not processed here because it may contain // enum shorthands (.value) that require array element type context. + // `[...base, e1, e2]` — spread / update syntax + if expr.update_expr !is ast.EmptyExpr { + update_type := c.expr(expr.update_expr) + mut update_base := resolve_alias(update_type) + if update_base is Pointer { + update_base = resolve_alias((update_base as Pointer).base_type) + } + if update_base !is Array { + c.error_with_pos('invalid array update: non-array type `${update_type.name()}`', + expr.pos) + return Type(void_) + } + array_t := update_base as Array + elem_type := array_t.elem_type + for elem_expr in expr.exprs { + expected_type_prev := c.expected_type + c.expected_type = to_optional_type(elem_type) + got_type := c.expr(elem_expr) + c.expected_type = expected_type_prev + if !c.check_types(elem_type, got_type) { + c.error_with_pos('expecting element of type: ${elem_type.name()}, got ${got_type.name()}', + expr.pos) + } + } + return Type(Array{ + elem_type: elem_type + }) + } // `[1,2,3,4]` if expr.exprs.len > 0 { expected_type_prev := c.expected_type -- 2.39.5