From 5b492e26ddea52b99200901323d638d166697dab Mon Sep 17 00:00:00 2001
From: Delyan Angelov <delian66@gmail.com>
Date: Thu, 24 Mar 2022 23:37:13 +0200
Subject: [PATCH] builtin: support a.flags.set(.noslices | .noshrink), use it
 in the particles example (#13818)

---
 .../sokol/particles/modules/particle/system.v | 18 +++++-
 vlib/builtin/array.v                          | 14 ++++-
 vlib/builtin/array_shrinkage_test.v           | 57 +++++++++++++++++++
 3 files changed, 86 insertions(+), 3 deletions(-)
 create mode 100644 vlib/builtin/array_shrinkage_test.v

diff --git a/examples/sokol/particles/modules/particle/system.v b/examples/sokol/particles/modules/particle/system.v
index 51874aa527..c319e61771 100644
--- a/examples/sokol/particles/modules/particle/system.v
+++ b/examples/sokol/particles/modules/particle/system.v
@@ -19,8 +19,8 @@ mut:
 }
 
 pub fn (mut s System) init(sc SystemConfig) {
-	unsafe { s.pool.flags.set(.noslices) }
-	unsafe { s.bin.flags.set(.noslices) }
+	unsafe { s.pool.flags.set(.noslices | .noshrink) }
+	unsafe { s.bin.flags.set(.noslices | .noshrink) }
 	for i := 0; i < sc.pool; i++ {
 		p := new(vec2.Vec2{f32(s.width) * 0.5, f32(s.height) * 0.5})
 		s.bin << p
@@ -29,12 +29,19 @@ pub fn (mut s System) init(sc SystemConfig) {
 
 pub fn (mut s System) update(dt f64) {
 	mut p := &Particle(0)
+	mut moved := 0
 	for i := 0; i < s.pool.len; i++ {
 		p = s.pool[i]
 		p.update(dt)
 		if p.is_dead() {
 			s.bin << p
 			s.pool.delete(i)
+			moved++
+		}
+	}
+	$if trace_moves_spool_to_sbin ? {
+		if moved != 0 {
+			eprintln('${moved:4} particles s.pool -> s.bin')
 		}
 	}
 }
@@ -64,6 +71,7 @@ pub fn (mut s System) explode(x f32, y f32) {
 	mut reserve := 500
 	center := vec2.Vec2{x, y}
 	mut p := &Particle(0)
+	mut moved := 0
 	for i := 0; i < s.bin.len && reserve > 0; i++ {
 		p = s.bin[i]
 		p.reset()
@@ -75,8 +83,14 @@ pub fn (mut s System) explode(x f32, y f32) {
 		p.life_time = rand.f64_in_range(500, 2000) or { 500 }
 		s.pool << p
 		s.bin.delete(i)
+		moved++
 		reserve--
 	}
+	$if trace_moves_sbin_to_spool ? {
+		if moved != 0 {
+			eprintln('${moved:4} particles s.bin -> s.pool')
+		}
+	}
 }
 
 pub fn (mut s System) free() {
diff --git a/vlib/builtin/array.v b/vlib/builtin/array.v
index e29e50d478..e6253d5eb3 100644
--- a/vlib/builtin/array.v
+++ b/vlib/builtin/array.v
@@ -22,7 +22,8 @@ pub mut:
 
 [flag]
 pub enum ArrayFlags {
-	noslices
+	noslices // when <<, `.noslices` will free the old data block immediately (you have to be sure, that there are *no slices* to that specific array). TODO: integrate with reference counting/compiler support for the static cases.
+	noshrink // when `.noslices` and `.noshrink` are *both set*, .delete(x) will NOT allocate new memory and free the old. It will just move the elements in place, and adjust .len.
 }
 
 // Internal function, used by V (`nums := []int`)
@@ -283,6 +284,14 @@ pub fn (mut a array) delete_many(i int, size int) {
 			panic('array.delete: index out of range (i == $i$endidx, a.len == $a.len)')
 		}
 	}
+	if a.flags.all(.noshrink | .noslices) {
+		unsafe {
+			vmemmove(&byte(a.data) + i * a.element_size, &byte(a.data) + (i + size) * a.element_size,
+				(a.len - i - size) * a.element_size)
+		}
+		a.len -= size
+		return
+	}
 	// Note: if a is [12,34], a.len = 2, a.delete(0)
 	// should move (2-0-1) elements = 1 element (the 34) forward
 	old_data := a.data
@@ -441,6 +450,8 @@ fn (a array) slice(start int, _end int) array {
 			panic('array.slice: slice bounds out of range ($start < 0)')
 		}
 	}
+	// TODO: integrate reference counting
+	// a.flags.clear(.noslices)
 	offset := start * a.element_size
 	data := unsafe { &byte(a.data) + offset }
 	l := end - start
@@ -461,6 +472,7 @@ fn (a array) slice(start int, _end int) array {
 // that get the last 3 elements of the array otherwise it return an empty array.
 // This function always return a valid array.
 fn (a array) slice_ni(_start int, _end int) array {
+	// a.flags.clear(.noslices)
 	mut end := _end
 	mut start := _start
 
diff --git a/vlib/builtin/array_shrinkage_test.v b/vlib/builtin/array_shrinkage_test.v
new file mode 100644
index 0000000000..84893fe864
--- /dev/null
+++ b/vlib/builtin/array_shrinkage_test.v
@@ -0,0 +1,57 @@
+fn show_array(name string, a []int) {
+	eprintln('${name:10} .flags: ${a.flags:34} | .cap: ${a.cap:2} | .len: ${a.len:2} | .data: $a.data | $a')
+}
+
+fn trace_delete_elements(name string, mut a []int) int {
+	a.delete_many(5, 3)
+	show_array(name, a)
+	a << 55
+	show_array(name, a)
+	a << 66
+	show_array(name, a)
+	a << 77
+	res := a.cap
+	eprintln('                                        <<   ${name:10} .cap: $a.cap >>')
+	show_array(name, a)
+	a << 88
+	show_array(name, a)
+	a << 99
+	show_array(name, a)
+	eprintln('-------------------------------')
+	return res
+}
+
+fn test_array_cap_shrinkage_after_deletion() {
+	mut a := [0]
+	mut middle_cap := 0
+
+	a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+	middle_cap = trace_delete_elements('normal', mut a)
+	assert middle_cap == 14
+	assert a.len == 12
+	assert a.cap == 14
+
+	a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+	unsafe { a.flags.set(.noslices) }
+	middle_cap = dump(trace_delete_elements('noslices', mut a))
+	assert middle_cap == 14
+	assert a.len == 12
+	assert a.cap == 14
+
+	a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+	unsafe { a.flags.set(.noshrink) }
+	middle_cap = dump(trace_delete_elements('noshrink', mut a))
+	assert middle_cap == 14
+	assert a.len == 12
+	assert a.cap == 14
+
+	// Note: when *both* flags are set, the memory block for the array
+	// should NOT shrink on deleting array elements, thus << after the
+	// deletion, will still have space (till .cap is reached).
+	a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+	unsafe { a.flags.set(.noslices | .noshrink) }
+	middle_cap = dump(trace_delete_elements('both', mut a))
+	assert middle_cap == 10
+	assert a.len == 12
+	assert a.cap == 20
+}