builtin: implement array.sorted() and array.sorted_with_compare() methods, that do not modify their receivers (#19251)

This commit is contained in:
Delyan Angelov 2023-09-01 09:12:00 +03:00 committed by GitHub
parent d9a382fb32
commit 58b6ba81d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 234 additions and 38 deletions

View file

@ -15,6 +15,7 @@ const (
//
'vlib/builtin/int_test.v',
'vlib/builtin/array_test.v',
'vlib/builtin/array_sorted_test.v',
'vlib/builtin/float_test.v',
'vlib/builtin/byte_test.v',
'vlib/builtin/rune_test.v',

View file

@ -827,6 +827,12 @@ pub fn (a array) map(callback fn (voidptr) voidptr) array
// Example: array.sort(b.name < a.name) // will sort descending by the .name field
pub fn (mut a array) sort(callback fn (voidptr, voidptr) int)
// sorted returns a sorted copy of the original array. The original array is *NOT* modified.
// See also .sort() .
// Example: assert [9,1,6,3,9].sorted() == [1,3,6,9,9]
// Example: assert [9,1,6,3,9].sorted(b < a) == [9,9,6,3,1]
pub fn (a &array) sorted(callback fn (voidptr, voidptr) int) array
// sort_with_compare sorts the array in-place using the results of the
// given function to determine sort order.
//
@ -859,6 +865,20 @@ pub fn (mut a array) sort_with_compare(callback fn (voidptr, voidptr) int) {
}
}
// sorted_with_compare sorts a clone of the array, using the results of the
// given function to determine sort order. The original array is not modified.
// See also .sort_with_compare()
pub fn (a &array) sorted_with_compare(callback fn (voidptr, voidptr) int) array {
$if freestanding {
panic('sort does not work with -freestanding')
} $else {
mut r := a.clone()
unsafe { vqsort(r.data, usize(r.len), usize(r.element_size), callback) }
return r
}
return array{}
}
// contains determines whether an array includes a certain value among its elements
// It will return `true` if the array contains an element with this value.
// It is similar to `.any` but does not take an `it` expression.

View file

@ -0,0 +1,90 @@
fn test_sorted_immutable_original_should_not_change() {
a := ['hi', '1', '5', '3']
b := a.sorted()
assert a == ['hi', '1', '5', '3']
assert b == ['1', '3', '5', 'hi']
}
fn test_sorted_mutable_original_should_not_change() {
mut a := ['hi', '1', '5', '3']
b := a.sorted()
assert a == ['hi', '1', '5', '3']
assert b == ['1', '3', '5', 'hi']
}
fn test_sorted_reversed() {
aa := ['hi', '1', '5', '3']
bb := aa.sorted(a > b)
assert aa == ['hi', '1', '5', '3']
assert bb == ['hi', '5', '3', '1']
}
fn test_sorted_by_len() {
a := ['hi', 'abc', 'a', 'zzzzz']
c := a.sorted(a.len < b.len)
assert c == ['a', 'hi', 'abc', 'zzzzz']
}
fn test_sorted_can_be_called_on_an_array_literals() {
b := [5, 1, 9].sorted()
assert b == [1, 5, 9]
assert [5.0, 1.2, 9.4].sorted() == [1.2, 5.0, 9.4]
assert ['a', '00', 'z', 'dd', 'xyz'].sorted(a > b) == ['z', 'xyz', 'dd', 'a', '00']
assert ['a', '00', 'zzzzz', 'dddd', 'xyz'].sorted(a.len > b.len) == ['zzzzz', 'dddd', 'xyz',
'00', 'a']
assert ['a', '00', 'zzzzz', 'dddd', 'xyz'].sorted(a.len < b.len) == ['a', '00', 'xyz', 'dddd',
'zzzzz']
}
fn iarr() []int {
return [5, 1, 9, 1, 2]
}
fn test_sorted_can_be_called_on_the_result_of_a_fn() {
assert iarr().sorted() == [1, 1, 2, 5, 9]
assert iarr().sorted(a > b) == [9, 5, 2, 1, 1]
}
fn sum(prev int, curr int) int {
return prev + curr
}
fn test_sorting_2d_arrays() {
assert [[1, 2], [3, 4, 5], [2]].sorted(a.len > b.len) == [
[3, 4, 5],
[1, 2],
[2],
]
assert [[1, 2], [3, 4, 5], [2]].sorted(a.len < b.len) == [
[2],
[1, 2],
[3, 4, 5],
]
assert unsafe { [[1, 2], [3, 4, 5], [2]].sorted(a[0] > b[0]) } == [
[3, 4, 5],
[2],
[1, 2],
]
// assert [[1, 2], [3, 4, 5], [2]].sorted( a.reduce(sum) > b.reduce(sum) ) == ... // TODO
}
fn test_sorting_3d_arrays() {
assert [[][]int{}, [[2, 22], [2]], [[3, 33], [3333], [33, 34, 35]],
[[444]]].sorted(a.len > b.len) == [[[3, 33], [3333], [33, 34, 35]],
[[2, 22], [2]], [[444]], [][]int{}]
}
fn test_sorted_with_compare() {
aa := ['hi', '1', '5', '3']
bb := aa.sorted_with_compare(fn (a &string, b &string) int {
if a < b {
return -1
}
if a > b {
return 1
}
return 0
})
assert aa == ['hi', '1', '5', '3'], 'aa should stay unmodified'
assert bb == ['1', '3', '5', 'hi'], 'bb should be sorted, according to the custom comparison callback fn'
}

View file

@ -67,12 +67,12 @@ fn test_deleting() {
fn test_slice_delete() {
mut a := [1.5, 2.5, 3.25, 4.5, 5.75]
b := a[2..4]
b := unsafe { a[2..4] }
a.delete(0)
assert a == [2.5, 3.25, 4.5, 5.75]
assert b == [3.25, 4.5]
a = [3.75, 4.25, -1.5, 2.25, 6.0]
c := a[..3]
c := unsafe { a[..3] }
a.delete(2)
assert a == [3.75, 4.25, 2.25, 6.0]
assert c == [3.75, 4.25, -1.5]
@ -80,11 +80,11 @@ fn test_slice_delete() {
fn test_delete_many() {
mut a := [1, 2, 3, 4, 5, 6, 7, 8, 9]
b := a[2..6]
b := unsafe { a[2..6] }
a.delete_many(4, 3)
assert a == [1, 2, 3, 4, 8, 9]
assert b == [3, 4, 5, 6]
c := a[..a.len]
c := unsafe { a[..a.len] }
a.delete_many(2, 0) // this should just clone
a[1] = 17
assert a == [1, 17, 3, 4, 8, 9]

View file

@ -26,8 +26,13 @@ const (
)
pub const (
// array_builtin_methods contains a list of all methods on array, that return other typed arrays,
// i.e. that act as *pseudogeneric* methods, that need compiler support, so that the types of the results
// are properly checked.
// Note that methods that do not return anything, or that return known types, are not listed here, since they are just ordinary non generic methods.
array_builtin_methods = ['filter', 'clone', 'repeat', 'reverse', 'map', 'slice', 'sort',
'contains', 'index', 'wait', 'any', 'all', 'first', 'last', 'pop', 'delete']
'sorted', 'sorted_with_compare', 'contains', 'index', 'wait', 'any', 'all', 'first', 'last',
'pop', 'delete']
array_builtin_methods_chk = token.new_keywords_matcher_from_array_trie(array_builtin_methods)
// TODO: remove `byte` from this list when it is no longer supported
reserved_type_names = ['byte', 'bool', 'char', 'i8', 'i16', 'int', 'i64', 'u8', 'u16',

View file

@ -2117,7 +2117,7 @@ fn (mut c Checker) method_call(mut node ast.CallExpr) ast.Type {
}
}
}
if left_sym.info is ast.Array && method_name == 'sort_with_compare' {
if left_sym.info is ast.Array && method_name in ['sort_with_compare', 'sorted_with_compare'] {
elem_typ := left_sym.info.elem_type
arg_sym := c.table.sym(arg.typ)
if arg_sym.kind == .function {
@ -2126,13 +2126,13 @@ fn (mut c Checker) method_call(mut node ast.CallExpr) ast.Type {
if func_info.func.params[0].typ.nr_muls() != elem_typ.nr_muls() + 1 {
arg_typ_str := c.table.type_to_str(func_info.func.params[0].typ)
expected_typ_str := c.table.type_to_str(elem_typ.ref())
c.error('sort_with_compare callback function parameter `${func_info.func.params[0].name}` with type `${arg_typ_str}` should be `${expected_typ_str}`',
c.error('${method_name} callback function parameter `${func_info.func.params[0].name}` with type `${arg_typ_str}` should be `${expected_typ_str}`',
func_info.func.params[0].type_pos)
}
if func_info.func.params[1].typ.nr_muls() != elem_typ.nr_muls() + 1 {
arg_typ_str := c.table.type_to_str(func_info.func.params[1].typ)
expected_typ_str := c.table.type_to_str(elem_typ.ref())
c.error('sort_with_compare callback function parameter `${func_info.func.params[1].name}` with type `${arg_typ_str}` should be `${expected_typ_str}`',
c.error('${method_name} callback function parameter `${func_info.func.params[1].name}` with type `${arg_typ_str}` should be `${expected_typ_str}`',
func_info.func.params[1].type_pos)
}
}
@ -2564,6 +2564,20 @@ fn (mut c Checker) map_builtin_method_call(mut node ast.CallExpr, left_type ast.
return node.return_type
}
// ensure_same_array_return_type makes sure, that the return type of .clone(), .sorted(), .sorted_with_compare() etc,
// is `array_xxx`, instead of the plain `array` .
fn (mut c Checker) ensure_same_array_return_type(mut node ast.CallExpr, left_type ast.Type) {
node.receiver_type = left_type.ref()
if node.left.is_auto_deref_var() {
node.return_type = left_type.deref()
} else {
node.return_type = node.receiver_type.set_nr_muls(0)
}
if node.return_type.has_flag(.shared_f) {
node.return_type = node.return_type.clear_flag(.shared_f)
}
}
fn (mut c Checker) array_builtin_method_call(mut node ast.CallExpr, left_type ast.Type, left_sym ast.TypeSymbol) ast.Type {
method_name := node.name
mut elem_typ := ast.void_type
@ -2579,12 +2593,14 @@ fn (mut c Checker) array_builtin_method_call(mut node ast.CallExpr, left_type as
if method_name in ['filter', 'map', 'any', 'all'] {
// position of `it` doesn't matter
scope_register_it(mut node.scope, node.pos, elem_typ)
} else if method_name == 'sort' {
if node.left is ast.CallExpr {
c.error('the `sort()` method can be called only on mutable receivers, but `${node.left}` is a call expression',
node.pos)
} else if method_name == 'sort' || method_name == 'sorted' {
if method_name == 'sort' {
if node.left is ast.CallExpr {
c.error('the `sort()` method can be called only on mutable receivers, but `${node.left}` is a call expression',
node.pos)
}
c.fail_if_immutable(mut node.left)
}
c.fail_if_immutable(mut node.left)
// position of `a` and `b` doesn't matter, they're the same
scope_register_a_b(mut node.scope, node.pos, elem_typ)
@ -2593,25 +2609,26 @@ fn (mut c Checker) array_builtin_method_call(mut node ast.CallExpr, left_type as
} else if node.args.len == 1 {
if node.args[0].expr is ast.InfixExpr {
if node.args[0].expr.op !in [.gt, .lt] {
c.error('`.sort()` can only use `<` or `>` comparison', node.pos)
c.error('`.${method_name}()` can only use `<` or `>` comparison',
node.pos)
}
left_name := '${node.args[0].expr.left}'[0]
right_name := '${node.args[0].expr.right}'[0]
if left_name !in [`a`, `b`] || right_name !in [`a`, `b`] {
c.error('`.sort()` can only use `a` or `b` as argument, e.g. `arr.sort(a < b)`',
c.error('`.${method_name}()` can only use `a` or `b` as argument, e.g. `arr.${method_name}(a < b)`',
node.pos)
} else if left_name == right_name {
c.error('`.sort()` cannot use same argument', node.pos)
c.error('`.${method_name}()` cannot use same argument', node.pos)
}
if node.args[0].expr.left !in [ast.Ident, ast.SelectorExpr, ast.IndexExpr]
|| node.args[0].expr.right !in [ast.Ident, ast.SelectorExpr, ast.IndexExpr] {
c.error('`.sort()` can only use ident, index or selector as argument, \ne.g. `arr.sort(a < b)`, `arr.sort(a.id < b.id)`, `arr.sort(a[0] < b[0])`',
c.error('`.${method_name}()` can only use ident, index or selector as argument, \ne.g. `arr.${method_name}(a < b)`, `arr.${method_name}(a.id < b.id)`, `arr.${method_name}(a[0] < b[0])`',
node.pos)
}
} else {
c.error(
'`.sort()` requires a `<` or `>` comparison as the first and only argument' +
'\ne.g. `users.sort(a.id < b.id)`', node.pos)
'`.${method_name}()` requires a `<` or `>` comparison as the first and only argument' +
'\ne.g. `users.${method_name}(a.id < b.id)`', node.pos)
}
} else if !(c.table.sym(elem_typ).has_method('<')
|| c.table.unalias_num_type(elem_typ) in [ast.int_type, ast.int_type.ref(), ast.string_type, ast.string_type.ref(), ast.i8_type, ast.i16_type, ast.i64_type, ast.u8_type, ast.rune_type, ast.u16_type, ast.u32_type, ast.u64_type, ast.f32_type, ast.f64_type, ast.char_type, ast.bool_type, ast.float_literal_type, ast.int_literal_type]) {
@ -2682,16 +2699,21 @@ fn (mut c Checker) array_builtin_method_call(mut node ast.CallExpr, left_type as
if node.args.len != 0 {
c.error('`.clone()` does not have any arguments', node.args[0].pos)
}
// need to return `array_xxx` instead of `array`
// in ['clone', 'str'] {
node.receiver_type = left_type.ref()
if node.left.is_auto_deref_var() {
node.return_type = left_type.deref()
} else {
node.return_type = node.receiver_type.set_nr_muls(0)
}
if node.return_type.has_flag(.shared_f) {
node.return_type = node.return_type.clear_flag(.shared_f)
c.ensure_same_array_return_type(mut node, left_type)
} else if method_name == 'sorted' {
c.ensure_same_array_return_type(mut node, left_type)
} else if method_name == 'sorted_with_compare' {
c.ensure_same_array_return_type(mut node, left_type)
// Inject a (voidptr) cast for the callback argument, to pass -cstrict, otherwise:
// error: incompatible function pointer types passing
// 'int (string *, string *)' (aka 'int (struct string *, struct string *)')
// to parameter of type 'int (*)(voidptr, voidptr)' (aka 'int (*)(void *, void *)')
node.args[0].expr = ast.CastExpr{
expr: node.args[0].expr
typ: ast.voidptr_type
typname: 'voidptr'
expr_type: c.expr(mut node.args[0].expr)
pos: node.pos
}
} else if method_name == 'sort' {
node.return_type = ast.void_type

View file

@ -585,6 +585,30 @@ fn (mut g Gen) gen_array_map(node ast.CallExpr) {
g.inside_lambda = false
}
// `susers := users.sorted(a.age < b.age)`
fn (mut g Gen) gen_array_sorted(node ast.CallExpr) {
past := g.past_tmp_var_new()
defer {
g.past_tmp_var_done(past)
}
atype := g.typ(node.return_type)
sym := g.table.sym(node.return_type)
info := sym.info as ast.Array
depth := g.get_array_depth(info.elem_type)
g.write('${atype} ${past.tmp_var} = array_clone_to_depth(ADDR(${atype},')
g.expr(node.left)
g.writeln('), ${depth});')
unsafe {
node.left = ast.Expr(ast.Ident{
name: past.tmp_var
})
}
g.gen_array_sort(node)
g.writeln(';')
}
// `users.sort(a.age < b.age)`
fn (mut g Gen) gen_array_sort(node ast.CallExpr) {
// println('filter s="$s"')

View file

@ -897,6 +897,10 @@ fn (mut g Gen) gen_array_method_call(node ast.CallExpr, left_type ast.Type) bool
g.gen_array_sort(node)
return true
}
'sorted' {
g.gen_array_sorted(node)
return true
}
'insert' {
g.gen_array_insert(node)
return true
@ -1303,7 +1307,7 @@ fn (mut g Gen) method_call(node ast.CallExpr) {
receiver_type_name = 'map'
}
if final_left_sym.kind == .array && !(left_sym.kind == .alias && left_sym.has_method(node.name))
&& node.name in ['clear', 'repeat', 'sort_with_compare', 'free', 'push_many', 'trim', 'first', 'last', 'pop', 'clone', 'reverse', 'slice', 'pointers'] {
&& node.name in ['clear', 'repeat', 'sort_with_compare', 'sorted_with_compare', 'free', 'push_many', 'trim', 'first', 'last', 'pop', 'clone', 'reverse', 'slice', 'pointers'] {
if !(left_sym.info is ast.Alias && typ_sym.has_method(node.name)) {
// `array_Xyz_clone` => `array_clone`
receiver_type_name = 'array'

View file

@ -0,0 +1,30 @@
module c
struct PastTmpVar {
mut:
tmp_var string
s string
s_ends_with_ln bool
}
fn (mut g Gen) past_tmp_var_new() PastTmpVar {
tmp_var := g.new_tmp_var()
mut s := g.go_before_stmt(0)
s_ends_with_ln := s.ends_with('\n')
s = s.trim_space()
g.empty_line = true
return PastTmpVar{
tmp_var: tmp_var
s: s
s_ends_with_ln: s_ends_with_ln
}
}
fn (mut g Gen) past_tmp_var_done(p &PastTmpVar) {
if p.s_ends_with_ln {
g.writeln(p.s)
} else {
g.write(p.s)
}
g.write(p.tmp_var)
}

View file

@ -34,9 +34,9 @@ struct Mapping {
struct Mappings {
mut:
sorted bool
last Mapping
values []Mapping
is_sorted bool
last Mapping
values []Mapping
}
fn new_mappings() Mappings {
@ -47,7 +47,7 @@ fn new_mappings() Mappings {
gen_line: 0
}
}
sorted: true
is_sorted: true
}
}
@ -55,7 +55,7 @@ fn new_mappings() Mappings {
fn (mut m Mappings) add_mapping(gen_line u32, gen_column u32, sources_ind u32, source_position SourcePositionType, names_ind NameIndexType) {
if !(gen_line > m.last.gen_line
|| (gen_line == m.last.gen_line && gen_column >= m.last.gen_column)) {
m.sorted = false
m.is_sorted = false
}
m.values << Mapping{
GenPosition: GenPosition{
@ -71,7 +71,7 @@ fn (mut m Mappings) add_mapping(gen_line u32, gen_column u32, sources_ind u32, s
// Returns the flat, sorted array of mappings. The mappings are sorted by generated position.
fn (mut m Mappings) get_sorted_array() []Mapping {
if !m.sorted {
if !m.is_sorted {
panic('not implemented')
}
return m.values

View file

@ -3099,7 +3099,7 @@ fn (mut p Parser) dot_expr(left ast.Expr) ast.Expr {
p.name_error = true
}
is_filter := field_name in ['filter', 'map', 'any', 'all']
if is_filter || field_name == 'sort' {
if is_filter || field_name == 'sort' || field_name == 'sorted' {
p.open_scope()
}
// ! in mutable methods
@ -3163,7 +3163,7 @@ fn (mut p Parser) dot_expr(left ast.Expr) ast.Expr {
scope: p.scope
comments: comments
}
if is_filter || field_name == 'sort' {
if is_filter || field_name == 'sort' || field_name == 'sorted' {
p.close_scope()
}
return mcall_expr