From 6f52e7050ba621e04e3102ddfafcde1398db2b55 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Tue, 14 Apr 2026 12:45:31 +0300 Subject: [PATCH] all: Allow negative indexing of array like `arr#[-1]` (fixes #24066) --- vlib/builtin/array.v | 20 ++++++++++++ vlib/builtin/builtin.c.v | 5 +++ vlib/builtin/gated_array_string_test.v | 17 +++++++++- vlib/builtin/string.v | 10 ++++++ vlib/v/checker/checker.v | 13 +++++--- vlib/v/fmt/fmt.v | 6 ++-- vlib/v/fmt/tests/gated_array_keep.vv | 1 + vlib/v/gen/c/index.v | 44 +++++++++++++++++++------- 8 files changed, 96 insertions(+), 20 deletions(-) diff --git a/vlib/builtin/array.v b/vlib/builtin/array.v index 682d2c18e..4e528343a 100644 --- a/vlib/builtin/array.v +++ b/vlib/builtin/array.v @@ -28,6 +28,11 @@ pub enum ArrayFlags { nofree // `.data` will never be freed } +@[inline] +fn v_ni_index(i int, len int) int { + return if i < 0 { len + i } else { i } +} + // Internal function, used by V (`nums := []int`) fn __new_array(mylen int, cap int, elm_size int) array { panic_on_negative_len(mylen) @@ -493,6 +498,11 @@ fn (a array) get(i int) voidptr { } } +@[markused] +fn (a array) get_ni(i int) voidptr { + return a.get(v_ni_index(i, a.len)) +} + // Private function. Used to implement x = a[i] or { ... } fn (a array) get_with_check(i int) voidptr { if i < 0 || i >= a.len { @@ -503,6 +513,11 @@ fn (a array) get_with_check(i int) voidptr { } } +@[markused] +fn (a array) get_with_check_ni(i int) voidptr { + return a.get_with_check(v_ni_index(i, a.len)) +} + // first returns the first element of the `array`. // If the `array` is empty, this will panic. // However, `a[0]` returns an error object @@ -770,6 +785,11 @@ fn (mut a array) set(i int, val voidptr) { unsafe { vmemcpy(&u8(a.data) + u64(a.element_size) * u64(i), val, a.element_size) } } +@[markused] +fn (mut a array) set_ni(i int, val voidptr) { + a.set(v_ni_index(i, a.len), val) +} + fn (mut a array) push(val voidptr) { if a.len < 0 { panic('array.push: negative len') diff --git a/vlib/builtin/builtin.c.v b/vlib/builtin/builtin.c.v index 384f28c5f..ad7b06d97 100644 --- a/vlib/builtin/builtin.c.v +++ b/vlib/builtin/builtin.c.v @@ -74,6 +74,11 @@ fn v_fixed_index(i int, len int) int { return i } +@[inline; markused] +fn v_fixed_index_ni(i int, len int) int { + return v_fixed_index(v_ni_index(i, len), len) +} + // arguments returns the command line arguments, used for starting the current program as a V array of strings. // The first string in the array (index 0), is the name of the program, used for invoking the program. // The second string in the array (index 1), if it exists, is the first argument to the program, etc. diff --git a/vlib/builtin/gated_array_string_test.v b/vlib/builtin/gated_array_string_test.v index 394d5dc99..981322ba6 100644 --- a/vlib/builtin/gated_array_string_test.v +++ b/vlib/builtin/gated_array_string_test.v @@ -1,16 +1,27 @@ fn test_gated_arrays() { a := [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + assert a#[-1] == 9 + assert a#[1] == 1 assert a#[-1..] == [9] assert a#[..-9] == [0] assert a#[-9..-7] == [1, 2] assert a#[-2..] == [8, 9] + missing := a#[-11] or { 42 } + assert missing == 42 + + mut b := [1, 2, 3] + b#[-1] = 100 + assert b == [1, 2, 100] // fixed array - a1 := [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]! + mut a1 := [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]! + assert a1#[-1] == 9 assert a1#[-1..] == [9] assert a1#[..-9] == [0] assert a1#[-9..-7] == [1, 2] assert a1#[-2..] == [8, 9] + a1#[-1] = 100 + assert a1[9] == 100 // empty array assert a#[-3..-4] == [] // start > end @@ -21,10 +32,14 @@ fn test_gated_arrays() { fn test_gated_strings() { a := '0123456789' + assert a#[-1] == `9` + assert a#[1] == `1` assert a#[-1..] == '9' assert a#[..-9] == '0' assert a#[-9..-7] == '12' assert a#[-2..] == '89' + missing := a#[-11] or { `x` } + assert missing == `x` // empty string assert a#[-3..-4] == '' // start > end diff --git a/vlib/builtin/string.v b/vlib/builtin/string.v index 734c97325..b0bf13ac4 100644 --- a/vlib/builtin/string.v +++ b/vlib/builtin/string.v @@ -2156,6 +2156,11 @@ fn (s string) at(idx int) u8 { return unsafe { s.str[idx] } } +@[markused] +fn (s string) at_ni(idx int) u8 { + return s.at(v_ni_index(idx, s.len)) +} + // version of `at()` that is used in `a[i] or {` // return an error when the index is out of range fn (s string) at_with_check(idx int) ?u8 { @@ -2167,6 +2172,11 @@ fn (s string) at_with_check(idx int) ?u8 { } } +@[markused] +fn (s string) at_with_check_ni(idx int) ?u8 { + return s.at_with_check(v_ni_index(idx, s.len)) +} + // Check if a string is an octal value. Returns 'true' if it is, or 'false' if it is not @[direct_array_access] pub fn (str string) is_oct() bool { diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 759ac5733..54bb11f12 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -7469,6 +7469,10 @@ fn (mut c Checker) index_expr(mut node ast.IndexExpr) ast.Type { } } else { // [1] if typ_sym.kind == .map { + if node.is_gated { + c.error('`#[]` negative indexing is only supported for arrays, fixed arrays, and strings', + node.pos) + } info := typ_sym.info as ast.Map old_expected_type := c.expected_type c.expected_type = info.key_type @@ -7491,11 +7495,12 @@ fn (mut c Checker) index_expr(mut node ast.IndexExpr) ast.Type { } } else { index_type := c.expr(mut node.index) - // for [1] case #[1] is not allowed! - if node.is_gated == true { - c.error('`#[]` allowed only for ranges', node.pos) + if node.is_gated && (typ.is_ptr() || typ.is_pointer() + || typ_sym.kind !in [.array, .array_fixed, .string]) { + c.error('`#[]` negative indexing is only supported for arrays, fixed arrays, and strings', + node.pos) } - c.check_index(typ_sym, node.index, index_type, node.pos, false, false) + c.check_index(typ_sym, node.index, index_type, node.pos, false, node.is_gated) } value_type := c.table.value_type(typ) if value_type != ast.void_type { diff --git a/vlib/v/fmt/fmt.v b/vlib/v/fmt/fmt.v index 1a0b0450d..cac03a0a3 100644 --- a/vlib/v/fmt/fmt.v +++ b/vlib/v/fmt/fmt.v @@ -2544,10 +2544,8 @@ pub fn (mut f Fmt) if_guard_expr(node ast.IfGuardExpr) { pub fn (mut f Fmt) index_expr(node ast.IndexExpr) { f.expr(node.left) - if node.index is ast.RangeExpr { - if node.index.is_gated { - f.write('#') - } + if node.is_gated { + f.write('#') } last_index_expr_state := f.is_index_expr f.is_index_expr = true diff --git a/vlib/v/fmt/tests/gated_array_keep.vv b/vlib/v/fmt/tests/gated_array_keep.vv index b83404857..55b078d13 100644 --- a/vlib/v/fmt/tests/gated_array_keep.vv +++ b/vlib/v/fmt/tests/gated_array_keep.vv @@ -1,4 +1,5 @@ a := [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +assert a#[-1] == 9 assert a#[-1..] == [9] assert a#[..-9] == [0] assert a#[-9..-7] == [1, 2] diff --git a/vlib/v/gen/c/index.v b/vlib/v/gen/c/index.v index babf6df06..a6a1aa606 100644 --- a/vlib/v/gen/c/index.v +++ b/vlib/v/gen/c/index.v @@ -38,12 +38,22 @@ fn (mut g Gen) index_expr(node ast.IndexExpr) { g.index_of_map(node, sym) } else if sym.kind == .string && !node.left_type.is_ptr() { gen_or := node.or_expr.kind != .absent || node.is_option + string_at_fn := if node.is_gated { + 'builtin__string_at_ni' + } else { + 'builtin__string_at' + } + string_at_with_check_fn := if node.is_gated { + 'builtin__string_at_with_check_ni' + } else { + 'builtin__string_at_with_check' + } if gen_or { tmp_opt := g.new_tmp_var() cur_line := g.go_before_last_stmt() g.out.write_string(util.tabs(g.indent)) opt_elem_type := g.styp(ast.u8_type.set_flag(.option)) - g.write('${opt_elem_type} ${tmp_opt} = builtin__string_at_with_check(') + g.write('${opt_elem_type} ${tmp_opt} = ${string_at_with_check_fn}(') g.expr(ast.Expr(node.left)) g.write(', ') g.expr(node.index) @@ -53,14 +63,15 @@ fn (mut g Gen) index_expr(node ast.IndexExpr) { } g.write('\n${cur_line}*(byte*)&${tmp_opt}.data') } else { - is_direct_array_access := g.is_direct_array_access || node.is_direct + is_direct_array_access := !node.is_gated + && (g.is_direct_array_access || node.is_direct) if is_direct_array_access { g.expr(ast.Expr(node.left)) g.write('.str[ ') g.expr(node.index) g.write(']') } else { - g.write('builtin__string_at(') + g.write('${string_at_fn}(') g.expr(ast.Expr(node.left)) g.write(', ') g.expr(node.index) @@ -249,22 +260,29 @@ fn (mut g Gen) index_of_array(node ast.IndexExpr, sym ast.TypeSymbol) { elem_type_str := if elem_sym.kind == .function { 'voidptr' } else { g.styp(elem_type) } result_type_str := if result_sym.kind == .function { 'voidptr' } else { g.styp(result_type) } left_is_shared := array_left_type.has_flag(.shared_f) + array_get_fn := if node.is_gated { 'builtin__array_get_ni' } else { 'builtin__array_get' } + array_get_with_check_fn := if node.is_gated { + 'builtin__array_get_with_check_ni' + } else { + 'builtin__array_get_with_check' + } + array_set_fn := if node.is_gated { 'builtin__array_set_ni' } else { 'builtin__array_set' } // `vals[i].field = x` is an exception and requires `array_get`: // `(*(Val*)array_get(vals, i)).field = x;` if g.is_assign_lhs && node.is_setter { - is_direct_array_access := g.is_direct_array_access || node.is_direct + is_direct_array_access := !node.is_gated && (g.is_direct_array_access || node.is_direct) is_op_assign := g.assign_op != .assign && info.elem_type != ast.string_type if is_direct_array_access { g.write('((${elem_type_str}*)') } else if is_op_assign { - g.write('(*(${elem_type_str}*)builtin__array_get(') + g.write('(*(${elem_type_str}*)${array_get_fn}(') if left_is_ptr && !left_is_shared { g.write('*') } } else { g.cur_indexexpr << node.pos.pos g.is_arraymap_set = true // special handling of assign_op and closing with '})' - g.write('builtin__array_set(') + g.write('${array_set_fn}(') if !left_is_ptr || left_is_shared { g.write('&') } @@ -321,7 +339,7 @@ fn (mut g Gen) index_of_array(node ast.IndexExpr, sym ast.TypeSymbol) { } } } else { - is_direct_array_access := g.is_direct_array_access || node.is_direct + is_direct_array_access := !node.is_gated && (g.is_direct_array_access || node.is_direct) is_fn_index_call := g.is_fn_index_call && elem_sym.info is ast.FnType // do not clone inside `opt_ok(opt_ok(&(string[]) {..})` before returns needs_clone := info.elem_type == ast.string_type_idx && g.is_autofree && !(g.inside_return @@ -338,7 +356,7 @@ fn (mut g Gen) index_of_array(node ast.IndexExpr, sym ast.TypeSymbol) { tmp_opt := if gen_or { g.new_tmp_var() } else { '' } tmp_opt_ptr := if gen_or { g.new_tmp_var() } else { '' } if gen_or { - g.write('${elem_type_str}* ${tmp_opt_ptr} = (${elem_type_str}*)(builtin__array_get_with_check(') + g.write('${elem_type_str}* ${tmp_opt_ptr} = (${elem_type_str}*)(${array_get_with_check_fn}(') if left_is_ptr && !left_is_shared { g.write('*') } @@ -353,7 +371,7 @@ fn (mut g Gen) index_of_array(node ast.IndexExpr, sym ast.TypeSymbol) { if is_direct_array_access { g.write(')((${elem_type_str}*)') } else { - g.write(')(*(${elem_type_str}*)builtin__array_get(') + g.write(')(*(${elem_type_str}*)${array_get_fn}(') } } if left_is_ptr && !left_is_shared && !is_direct_array_access { @@ -362,7 +380,7 @@ fn (mut g Gen) index_of_array(node ast.IndexExpr, sym ast.TypeSymbol) { } else if is_direct_array_access { g.write('((${elem_type_str}*)') } else { - g.write('(*(${elem_type_str}*)builtin__array_get(') + g.write('(*(${elem_type_str}*)${array_get_fn}(') if left_is_ptr && !left_is_shared { g.write('*') } @@ -481,7 +499,11 @@ fn (mut g Gen) index_of_fixed_array(node ast.IndexExpr, sym ast.TypeSymbol) { } } g.write('[') - if g.is_direct_array_access || g.pref.translated || node.index is ast.IntegerLiteral { + if node.is_gated { + g.write('builtin__v_fixed_index_ni(') + g.expr(node.index) + g.write(', ${info.size})') + } else if g.is_direct_array_access || g.pref.translated || node.index is ast.IntegerLiteral { g.expr(node.index) } else { // bounds check -- 2.39.5