Serene Runtime 1.0.0-dev
C runtime for the Serene programming language
Loading...
Searching...
No Matches
array_list_tests.h
Go to the documentation of this file.
1/* -*- C -*-
2 * Serene programming language
3 * Copyright (C) 2019-2026 Sameer Rahmani <lxsameer@lxsameer.com>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
19#pragma once
20
21#include <stdio.h>
22
23#include <serene/rt/context.h>
25
26#include "base.h"
27
28#define AL_TESTS(X) \
29 X("array_list::empty", test_al_empty), \
30 X("array_list::push_less_than_32", test_al_push_less_than_32), \
31 X("array_list::push_more_than_32", test_al_push_more_than_32), \
32 X("array_list::tail_to_trie_boundary", test_al_push_tail_to_trie_boundary), \
33 X("array_list::get_out_of_bounds", test_al_get_out_of_bounds), \
34 X("array_list::update", test_al_update), X("array_list::remove", test_al_remove), \
35 X("array_list::remove_out_of_bounds", test_al_remove_out_of_bounds), \
36 X("array_list::remove_drain_and_push", test_al_remove_drain_and_push), \
37 X("array_list::for_each", test_al_for_each), \
38 X("array_list::for_each_early_exit", test_al_for_each_early_exit), \
39 X("array_list::for_each_empty", test_al_for_each_empty)
40
41#if defined(AL_DEBUG)
42# define AL_TEST_LOG(...) DBG("AL_TEST", __VA_ARGS__)
43#else
44# define AL_TEST_LOG(...)
45#endif
46
47typedef struct al_dummy {
48 int foo;
49 int bar;
51
52static void test_al_empty() {
53 MAKE_ENGINE(mm, engine);
54 MAKE_CONTEXT(engine, ctx);
55
56 auto s = array_list_empty(ctx);
57
58 TEST_CHECK(s.len == 0);
59 TEST_CHECK(s.tail_len == 0);
60 TEST_CHECK(s.root == nullptr);
61 TEST_CHECK(s.maybe_error == nullptr);
62
63 RELEASE_CONTEXT(ctx);
64 SHUTDOWN_ENGINE(mm, engine);
65}
66
68 MAKE_ENGINE(mm, engine);
69 MAKE_CONTEXT(engine, ctx);
70
71 auto s = array_list_empty(ctx);
72
73 for (size_t i = 0; i < 31; i++) {
74 auto g = ALLOC(ctx, al_dummy);
75 g->bar = (int)i + 1;
76 g->foo = (int)i + 2;
77 auto err = array_list_push(&s, g);
78 ASSERT_NULL(err);
79
80 TEST_CHECK(s.len == i + 1);
81 TEST_CHECK(s.tail_len == i + 1);
82 TEST_CHECK(s.root == nullptr);
83 TEST_CHECK(s.maybe_error == nullptr);
84 }
85
86 for (size_t i = 0; i < 30; i++) {
87 auto res = array_list_get(&s, i);
88 TEST_NO_ERROR(res);
89 ASSERT_NOT_NULL(res.data);
90
91 al_dummy *d = res.data;
92 TEST_CHECK(d->foo == (int)(i + 2));
93 TEST_CHECK(d->bar == (int)(i + 1));
94 }
95
96 RELEASE_CONTEXT(ctx);
97 SHUTDOWN_ENGINE(mm, engine);
98}
99
101 MAKE_ENGINE(mm, engine);
102 MAKE_CONTEXT(engine, ctx);
103
104 auto s = array_list_empty(ctx);
105
106 for (int i = 0; i < 2000; i++) {
107
108 al_dummy *d = ALLOC(ctx, al_dummy);
110 d->foo = i;
111 d->bar = 3000 - i;
112 auto err = array_list_push(&s, d);
113 ASSERT_NULL(err);
114 }
115
116 TEST_CHECK(s.len == 2000);
117 TEST_CHECK(s.tail_len == 16);
118 TEST_CHECK(s.root != nullptr);
119 TEST_CHECK(s.maybe_error == nullptr);
120
121 al_dummy *d1 = (al_dummy *)s.tail[0];
122
123 ASSERT_NOT_NULL(d1);
124 TEST_CHECK(d1->foo == 1984);
125 TEST_CHECK(d1->bar == 3000 - d1->foo);
126
127 al_dummy *d2 = (al_dummy *)s.tail[s.tail_len - 1];
128 ASSERT_NOT_NULL(d2);
129 TEST_CHECK(d2->foo == 1999);
130 TEST_CHECK(d2->bar == 3000 - d2->foo);
131
132 for (int i = 0; i < 2000; i++) {
133 auto res = array_list_get(&s, (size_t)i);
134 TEST_NO_ERROR(res);
135 ASSERT_NOT_NULL(res.data);
136
137 al_dummy *d = res.data;
138
139 // AL_TEST_LOG("%d %d %d", i, d->foo, d->bar);
140 TEST_CHECK(d->foo == i);
141 TEST_CHECK(d->bar == 3000 - i);
142 };
143
144 RELEASE_CONTEXT(ctx);
145 SHUTDOWN_ENGINE(mm, engine);
146}
147
148// Pushing element AL_BR + 1 moves the full tail into the trie and starts a
149// fresh tail. This isolates that transition, which the bulk push tests only
150// cross incidentally.
152 MAKE_ENGINE(mm, engine);
153 MAKE_CONTEXT(engine, ctx);
154
155 auto s = array_list_empty(ctx);
156
157 // Fill the tail exactly to AL_BR. Everything still lives in the tail.
158 for (int i = 0; i < 32; i++) {
159 al_dummy *d = ALLOC(ctx, al_dummy);
160 d->foo = i;
161 d->bar = 3000 - i;
162 auto err = array_list_push(&s, d);
163 ASSERT_NULL(err);
164 }
165
166 TEST_CHECK(s.len == 32);
167 TEST_CHECK(s.tail_len == 32);
168 TEST_CHECK(s.root == nullptr);
169 TEST_CHECK(s.maybe_error == nullptr);
170
171 // One more push moves the full tail into the trie and starts a fresh tail.
172 al_dummy *d = ALLOC(ctx, al_dummy);
173 d->foo = 32;
174 d->bar = 3000 - 32;
175 auto err = array_list_push(&s, d);
176 ASSERT_NULL(err);
177
178 TEST_CHECK(s.len == 33);
179 TEST_CHECK(s.tail_len == 1);
180 TEST_CHECK(s.root != nullptr);
181 TEST_CHECK(s.depth == 0);
182 TEST_CHECK(s.maybe_error == nullptr);
183
184 // The last element of the old tail now lives in the trie, the new one in the
185 // fresh tail, and the first element is still reachable.
186 auto first = array_list_get(&s, 0);
189 TEST_CHECK(((al_dummy *)first.data)->foo == 0);
190
191 auto last_moved = array_list_get(&s, 31);
192 TEST_NO_ERROR(last_moved);
193 ASSERT_NOT_NULL(last_moved.data);
194 TEST_CHECK(((al_dummy *)last_moved.data)->foo == 31);
195
196 auto in_tail = array_list_get(&s, 32);
197 TEST_NO_ERROR(in_tail);
198 ASSERT_NOT_NULL(in_tail.data);
199 TEST_CHECK(((al_dummy *)in_tail.data)->foo == 32);
200
201 RELEASE_CONTEXT(ctx);
202 SHUTDOWN_ENGINE(mm, engine);
203}
204
205// array_list_get must reject any index at or past len with INDEX_OUT_OF_BOUND
206// and no data, both on an empty list and after pushes.
208 MAKE_ENGINE(mm, engine);
209 MAKE_CONTEXT(engine, ctx);
210
211 auto s = array_list_empty(ctx);
212
213 // Empty list: index 0 is already out of bounds.
214 auto empty = array_list_get(&s, 0);
215 ASSERT_NOT_NULL(empty.maybe_error);
216 TEST_CHECK(empty.maybe_error->tag == INDEX_OUT_OF_BOUND);
217 ASSERT_NULL(empty.data);
218
219 for (int i = 0; i < 40; i++) {
220 al_dummy *d = ALLOC(ctx, al_dummy);
221 d->foo = i;
222 d->bar = 3000 - i;
223 auto err = array_list_push(&s, d);
224 ASSERT_NULL(err);
225 }
226
227 // The first invalid index is exactly len.
228 auto at_len = array_list_get(&s, s.len);
229 ASSERT_NOT_NULL(at_len.maybe_error);
230 TEST_CHECK(at_len.maybe_error->tag == INDEX_OUT_OF_BOUND);
231 ASSERT_NULL(at_len.data);
232
233 // A far larger index fails the same way and exercises the message sizing path.
234 auto far = array_list_get(&s, s.len + 1000);
235 ASSERT_NOT_NULL(far.maybe_error);
236 TEST_CHECK(far.maybe_error->tag == INDEX_OUT_OF_BOUND);
237 ASSERT_NULL(far.data);
238
239 // The last valid index still succeeds.
240 auto last = array_list_get(&s, s.len - 1);
241 TEST_NO_ERROR(last);
242 ASSERT_NOT_NULL(last.data);
243 TEST_CHECK(((al_dummy *)last.data)->foo == 39);
244
245 RELEASE_CONTEXT(ctx);
246 SHUTDOWN_ENGINE(mm, engine);
247}
248
249static void test_al_update() {
250 MAKE_ENGINE(mm, engine);
251 MAKE_CONTEXT(engine, ctx);
252
253 auto s = array_list_empty(ctx);
254
255 al_dummy *a = ALLOC(ctx, al_dummy);
256 a->foo = 1;
257 a->bar = 1;
259
260 al_dummy *b = ALLOC(ctx, al_dummy);
261 b->foo = 2;
262 b->bar = 2;
263
264 // a and b are distinct allocations, so distinct pointers.
265 TEST_CHECK(a != b);
266
267 // Precondition: slot 0 holds `a`.
268 auto before = array_list_get(&s, 0);
269 TEST_NO_ERROR(before);
270 TEST_CHECK(before.data == a);
271
272 // Update slot 0 to point at `b`.
274
275 // Postcondition: a fresh fetch must now yield `b`, not `a`.
276 auto after = array_list_get(&s, 0);
277 TEST_NO_ERROR(after);
278 TEST_CHECK(after.data == b);
279
280 RELEASE_CONTEXT(ctx);
281 SHUTDOWN_ENGINE(mm, engine);
282}
283
284// Removing a middle index shifts every later element down one and shrinks the
285// length by one, across the tail-to-trie boundary.
286static void test_al_remove() {
287 MAKE_ENGINE(mm, engine);
288 MAKE_CONTEXT(engine, ctx);
289
290 auto s = array_list_empty(ctx);
291
292 // foo carries the logical index so a shift is visible after removal.
293 for (int i = 0; i < 100; i++) {
294 al_dummy *d = ALLOC(ctx, al_dummy);
295 d->foo = i;
296 d->bar = 3000 - i;
298 }
299
300 auto err = array_list_remove(&s, 50);
301 ASSERT_NULL(err);
302 TEST_CHECK(s.len == 99);
303
304 // Elements before the hole are untouched, everything after slid down one.
305 for (int i = 0; i < 99; i++) {
306 auto res = array_list_get(&s, (size_t)i);
307 TEST_NO_ERROR(res);
308 ASSERT_NOT_NULL(res.data);
309 int expected = i < 50 ? i : i + 1;
310 TEST_CHECK(((al_dummy *)res.data)->foo == expected);
311 }
312
313 // The old last index is now out of bounds.
314 auto gone = array_list_get(&s, 99);
315 ASSERT_NOT_NULL(gone.maybe_error);
316 TEST_CHECK(gone.maybe_error->tag == INDEX_OUT_OF_BOUND);
317
318 // Removing the final element is the pop case: no shift, just a shrink.
319 ASSERT_NULL(array_list_remove(&s, s.len - 1));
320 TEST_CHECK(s.len == 98);
321 auto last = array_list_get(&s, s.len - 1);
322 TEST_NO_ERROR(last);
323 // Index 97 held foo 98 after the first removal, and dropping the final
324 // element does not disturb it.
325 TEST_CHECK(((al_dummy *)last.data)->foo == 98);
326
327 RELEASE_CONTEXT(ctx);
328 SHUTDOWN_ENGINE(mm, engine);
329}
330
331// array_list_remove rejects any index at or past len, on both an empty and a
332// populated list, without changing the length.
334 MAKE_ENGINE(mm, engine);
335 MAKE_CONTEXT(engine, ctx);
336
337 auto s = array_list_empty(ctx);
338
339 auto empty = array_list_remove(&s, 0);
340 ASSERT_NOT_NULL(empty);
341 TEST_CHECK(empty->tag == INDEX_OUT_OF_BOUND);
342 TEST_CHECK(s.len == 0);
343
344 for (int i = 0; i < 40; i++) {
345 al_dummy *d = ALLOC(ctx, al_dummy);
346 d->foo = i;
348 }
349
350 auto at_len = array_list_remove(&s, s.len);
351 ASSERT_NOT_NULL(at_len);
352 TEST_CHECK(at_len->tag == INDEX_OUT_OF_BOUND);
353 TEST_CHECK(s.len == 40);
354
355 RELEASE_CONTEXT(ctx);
356 SHUTDOWN_ENGINE(mm, engine);
357}
358
359// Repeatedly removing the front drains the tail many times over, which forces
360// array_list_pull_last_leaf, then pushing again reuses the drained (over-tall)
361// trie. Everything must still read back in order.
363 MAKE_ENGINE(mm, engine);
364 MAKE_CONTEXT(engine, ctx);
365
366 auto s = array_list_empty(ctx);
367
368 for (int i = 0; i < 2000; i++) {
369 al_dummy *d = ALLOC(ctx, al_dummy);
370 d->foo = i;
372 }
373
374 // Drop the front 1990 times; the survivors are the original 1990..1999.
375 for (int i = 0; i < 1990; i++) {
377 }
378 TEST_CHECK(s.len == 10);
379
380 for (int i = 0; i < 10; i++) {
381 auto res = array_list_get(&s, (size_t)i);
382 TEST_NO_ERROR(res);
383 ASSERT_NOT_NULL(res.data);
384 TEST_CHECK(((al_dummy *)res.data)->foo == 1990 + i);
385 }
386
387 // Push a fresh batch into the shrunken trie.
388 for (int i = 0; i < 2000; i++) {
389 al_dummy *d = ALLOC(ctx, al_dummy);
390 d->foo = 10000 + i;
392 }
393 TEST_CHECK(s.len == 2010);
394
395 // The ten survivors are still first, the new batch follows in order.
396 for (int i = 0; i < 10; i++) {
397 auto res = array_list_get(&s, (size_t)i);
398 TEST_NO_ERROR(res);
399 TEST_CHECK(((al_dummy *)res.data)->foo == 1990 + i);
400 }
401 for (int i = 0; i < 2000; i++) {
402 auto res = array_list_get(&s, (size_t)(10 + i));
403 TEST_NO_ERROR(res);
404 TEST_CHECK(((al_dummy *)res.data)->foo == 10000 + i);
405 }
406
407 RELEASE_CONTEXT(ctx);
408 SHUTDOWN_ENGINE(mm, engine);
409}
410
411// ---------------------------------------------------------------------------
412// for_each helpers — no userdata slot in the callback, so state is global.
413// These tests run serially so there is no race.
414// ---------------------------------------------------------------------------
415
416static size_t g_fe_count;
417static bool g_fe_order_ok;
418static size_t g_fe_stop_at;
419
420static srn_error_t *fe_all_visitor(array_list_t *al, size_t n, void *data) {
421 UNUSED(al);
422 g_fe_count++;
423
424 if (((al_dummy *)data)->foo != (int)n) {
425 g_fe_order_ok = false;
426 }
427
428 return nullptr;
429}
430
431static srn_error_t *fe_early_exit_visitor(array_list_t *al, size_t n, void *data) {
432 UNUSED(data);
433 g_fe_count++;
434
435 if (n == g_fe_stop_at) {
436 return ERR(al->ctx, INDEX_OUT_OF_BOUND, "stop");
437 }
438
439 return nullptr;
440}
441
442// for_each visits every element exactly once, in index order, across the
443// tail to trie boundary (100 elements: depth-1 trie + tail).
444static void test_al_for_each() {
445 MAKE_ENGINE(mm, engine);
446 MAKE_CONTEXT(engine, ctx);
447
448 auto s = array_list_empty(ctx);
449
450 for (int i = 0; i < 100; i++) {
451 al_dummy *d = ALLOC(ctx, al_dummy);
452 d->foo = i;
453 d->bar = 3000 - i;
455 }
456
457 g_fe_count = 0;
458 g_fe_order_ok = true;
459
460 auto err = array_list_for_each(&s, fe_all_visitor);
461 ASSERT_NULL(err);
462 TEST_CHECK(g_fe_count == 100);
464
465 RELEASE_CONTEXT(ctx);
466 SHUTDOWN_ENGINE(mm, engine);
467}
468
469// for_each stops and returns the callback's error the moment the callback
470// returns one, without visiting any later element.
472 MAKE_ENGINE(mm, engine);
473 MAKE_CONTEXT(engine, ctx);
474
475 auto s = array_list_empty(ctx);
476
477 for (int i = 0; i < 20; i++) {
478 al_dummy *d = ALLOC(ctx, al_dummy);
479 d->foo = i;
481 }
482
483 g_fe_count = 0;
484 g_fe_stop_at = 5;
485
487 ASSERT_NOT_NULL(err);
488 TEST_CHECK(err->tag == INDEX_OUT_OF_BOUND);
489 // The callback ran for indices 0..5 inclusive, so 6 calls total.
491
492 RELEASE_CONTEXT(ctx);
493 SHUTDOWN_ENGINE(mm, engine);
494}
495
496// for_each on an empty list returns null without invoking the callback at all.
498 MAKE_ENGINE(mm, engine);
499 MAKE_CONTEXT(engine, ctx);
500
501 auto s = array_list_empty(ctx);
502
503 g_fe_count = 0;
504
505 auto err = array_list_for_each(&s, fe_all_visitor);
506 ASSERT_NULL(err);
508
509 RELEASE_CONTEXT(ctx);
510 SHUTDOWN_ENGINE(mm, engine);
511}
#define TEST_CHECK(cond)
Definition acutest.h:95
int n
Definition acutest.h:525
array_list_maybe_element_t array_list_get(const array_list_t *al, array_list_index_t n)
Negative index is not supported.
Definition array_list.c:235
srn_error_t * array_list_update(array_list_t *al, array_list_index_t n, array_list_elem_t x)
Update the given index n of the array list al with the new element x.
Definition array_list.c:246
srn_error_t * array_list_remove(array_list_t *al, array_list_index_t n)
Remove the element at the given index n from the array list al, shifting every later element down one...
Definition array_list.c:301
array_list_t array_list_empty(const srn_context_t *ctx)
Create an empty array list in the given context ctx.
Definition array_list.c:101
srn_error_t * array_list_push(array_list_t *al, array_list_elem_t x)
Push the given element x to the end of array_list al.
Definition array_list.c:114
srn_error_t * array_list_for_each(array_list_t *al, array_list_for_each_f_t f)
Run the give function f for every element in the given array list al.
Definition array_list.c:327
A bit-partitioned trie with a tail buffer, used as a growable indexed sequence in the runtime.
static void test_al_remove()
static void test_al_get_out_of_bounds()
static void test_al_remove_drain_and_push()
static void test_al_for_each()
static srn_error_t * fe_all_visitor(array_list_t *al, size_t n, void *data)
static void test_al_push_more_than_32()
static size_t g_fe_stop_at
static void test_al_for_each_empty()
static void test_al_push_tail_to_trie_boundary()
static srn_error_t * fe_early_exit_visitor(array_list_t *al, size_t n, void *data)
static void test_al_for_each_early_exit()
static void test_al_remove_out_of_bounds()
static void test_al_update()
static void test_al_empty()
static bool g_fe_order_ok
static size_t g_fe_count
static void test_al_push_less_than_32()
#define RELEASE_CONTEXT(x)
Definition base.h:48
#define ASSERT_NOT_NULL(x)
Definition base.h:30
#define SHUTDOWN_ENGINE(mm, engine)
Definition base.h:40
#define TEST_NO_ERROR(x)
Definition base.h:32
#define MAKE_ENGINE(mm, engine)
Definition base.h:34
#define ASSERT_NULL(x)
Definition base.h:31
#define MAKE_CONTEXT(engine, x)
Definition base.h:44
#define ALLOC(ctx, T)
Definition context.h:95
@ INDEX_OUT_OF_BOUND
Definition errors.h:98
#define ERR(ctx, err, msg)
Definition errors.h:223
auto first
const srn_context_t * ctx
The context that owns every allocation the array list retains.
Definition array_list.h:183
A runtime error, a tag classifying the failure and a human-readable message.
Definition errors.h:173
#define UNUSED(x)
Definition utils.h:45