Fixed unit tests + fixed different compilation flags' combinations so that code compiles and works properly + added unit tests execution into a github actions workflow

This commit is contained in:
Egor Sheremetov 2023-09-27 03:45:58 +02:00
parent ec9beed470
commit 8b073cbd48
8 changed files with 116 additions and 61 deletions

View file

@ -38,3 +38,4 @@ jobs:
- run: eval $(opam env)
- run: opam exec -- make
- run: opam exec -- make regression-all
- run: opam exec -- make unit_tests

View file

@ -9,6 +9,9 @@ all:
$(MAKE) -C runtime
$(MAKE) -C byterun
$(MAKE) -C stdlib
$(MAKE) -C runtime unit_tests.o
$(MAKE) -C runtime invariants_check.o
$(MAKE) -C runtime invariants_check_debug_print.o
STD_FILES=$(shell ls stdlib/*.[oi] stdlib/*.lama runtime/runtime.a runtime/Std.i)
@ -38,6 +41,11 @@ regression-lama-in-lama: all
cp -R stdlib/* tmp-lama
$(MAKE) -C lama-compiler
unit_tests:
./runtime/unit_tests.o
./runtime/invariants_check.o
./runtime/invariants_check_debug_print.o
clean:
$(MAKE) clean -C src
$(MAKE) clean -C runtime

View file

@ -1,23 +1,38 @@
CC=gcc
FLAGS=-no-pie -m32 -g2 -fstack-protector-all -DLAMA_ENV
COMMON_FLAGS=-no-pie -m32 -g2 -fstack-protector-all
PROD_FLAGS=$(COMMON_FLAGS) -DLAMA_ENV
TEST_FLAGS=$(COMMON_FLAGS) -DDEBUG_VERSION
UNIT_TESTS_FLAGS=$(TEST_FLAGS)
INVARIANTS_CHECK_FLAGS=$(TEST_FLAGS) -DFULL_INVARIANT_CHECKS
# this target is the most important one, its' artefacts should be used as a runtime of Lama
all: gc_runtime.o gc.o runtime.o
ar rc runtime.a gc_runtime.o runtime.o gc.o
test.o: gc.c gc.h gc_runtime.s runtime.c runtime.h runtime_common.h virt_stack.c virt_stack.h test_main.c test_util.s
$(CC) -o test.o -DDEBUG_VERSION $(FLAGS) gc.c gc_runtime.s virt_stack.c ext_arr.c runtime.c test_main.c test_util.s
# this is a target that runs unit tests, scenarios are written in a single file `test_main.c`
unit_tests.o: gc.c gc.h gc_runtime.s runtime.c runtime.h runtime_common.h virt_stack.c virt_stack.h test_main.c test_util.s
$(CC) -o unit_tests.o $(UNIT_TESTS_FLAGS) gc.c gc_runtime.s virt_stack.c runtime.c test_main.c test_util.s
# this target also runs unit tests but with additional expensive checks of GC invariants which aren't used in production version
invariants_check.o: gc.c gc.h gc_runtime.s runtime.c runtime.h runtime_common.h virt_stack.c virt_stack.h test_main.c test_util.s
$(CC) -o invariants_check.o $(INVARIANTS_CHECK_FLAGS) gc.c gc_runtime.s virt_stack.c runtime.c test_main.c test_util.s
# this target also runs unit tests but with additional expensive checks of GC invariants which aren't used in production version
# additionally, it prints debug information
invariants_check_debug_print.o: gc.c gc.h gc_runtime.s runtime.c runtime.h runtime_common.h virt_stack.c virt_stack.h test_main.c test_util.s
$(CC) -o invariants_check_debug_print.o $(INVARIANTS_CHECK_FLAGS) -DDEBUG_PRINT gc.c gc_runtime.s virt_stack.c runtime.c test_main.c test_util.s
virt_stack.o: virt_stack.h virt_stack.c
$(CC) $(FLAGS) -c virt_stack.c
$(CC) $(PROD_FLAGS) -c virt_stack.c
gc.o: gc.c gc.h
$(CC) -rdynamic $(FLAGS) -c gc.c
$(CC) -rdynamic $(PROD_FLAGS) -c gc.c
gc_runtime.o: gc_runtime.s
$(CC) $(FLAGS) -c gc_runtime.s
$(CC) $(PROD_FLAGS) -c gc_runtime.s
runtime.o: runtime.c runtime.h
$(CC) $(FLAGS) -c runtime.c
$(CC) $(PROD_FLAGS) -c runtime.c
clean:
$(RM) *.a *.o *~

View file

@ -10,7 +10,7 @@
- [x] Fix warnings in ML code
- [x] TODO: debug flag doesn't compile
- [x] Sexp: move the tag to be `contents[0]` instead of the word in sexp header; i.e. get rid of sexp as separate data structure
- [ ] Run Lama compiler on Lama
- [x] Run Lama compiler on Lama
- [ ] Add more stress tests (for graph-like structures) to `stdlib/regression` and unit tests
- [ ] Magic constants
- [ ] Normal documentation: a-la doxygen

View file

@ -16,7 +16,7 @@
static const size_t INIT_HEAP_SIZE = MINIMUM_HEAP_CAPACITY;
#ifdef FULL_INVARIANT_CHECKS
#ifdef DEBUG_VERSION
size_t cur_id = 0;
#endif
@ -49,12 +49,12 @@ void handler (int sig) {
}
void *alloc (size_t size) {
#ifdef FULL_INVARIANT_CHECKS
#ifdef DEBUG_VERSION
++cur_id;
#endif
size_t bytes_sz = size;
size = BYTES_TO_WORDS(size);
#ifdef DEBUG_VERSION
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr, "allocation of size %zu words (%zu bytes): ", size, bytes_sz);
#endif
void *p = gc_alloc_on_existing_heap(size);
@ -184,7 +184,7 @@ void *gc_alloc_on_existing_heap (size_t size) {
}
void *gc_alloc (size_t size) {
#ifdef DEBUG_VERSION
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr, "===============================GC cycle has started\n");
#endif
#ifdef FULL_INVARIANT_CHECKS
@ -217,32 +217,34 @@ void *gc_alloc (size_t size) {
fclose(heap_before_compaction);
fclose(heap_after_compaction);
#endif
#ifdef DEBUG_VERSION
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr, "===============================GC cycle has finished\n");
#endif
return gc_alloc_on_existing_heap(size);
}
void mark_phase (void) {
#ifdef DEBUG_VERSION
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr, "marking has started\n");
fprintf(stderr,
"__gc_root_scan_stack has started: gc_top=%p bot=%p\n",
__gc_stack_top,
__gc_stack_bottom);
(void *)__gc_stack_top,
(void *)__gc_stack_bottom);
#endif
__gc_root_scan_stack();
#ifdef DEBUG_VERSION
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr, "__gc_root_scan_stack has finished\n");
fprintf(stderr, "scan_extra_roots has started\n");
#endif
scan_extra_roots();
#ifdef DEBUG_VERSION
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr, "scan_extra_roots has finished\n");
fprintf(stderr, "scan_global_area has started\n");
#endif
#ifdef LAMA_ENV
scan_global_area();
#ifdef DEBUG_VERSION
#endif
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr, "scan_global_area has finished\n");
fprintf(stderr, "marking has finished\n");
#endif
@ -254,8 +256,7 @@ void compact_phase (size_t additional_size) {
// all in words
size_t next_heap_size =
MAX(live_size * EXTRA_ROOM_HEAP_COEFFICIENT + additional_size, MINIMUM_HEAP_CAPACITY);
size_t next_heap_pseudo_size =
MAX(next_heap_size, heap.size); // this is weird but here is why it happens:
size_t next_heap_pseudo_size = MAX(next_heap_size, heap.size);
memory_chunk old_heap = heap;
heap.begin = mremap(
@ -275,7 +276,7 @@ void compact_phase (size_t additional_size) {
}
size_t compute_locations () {
#ifdef DEBUG_VERSION
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr, "GC compute_locations started\n");
#endif
size_t *free_ptr = heap.begin;
@ -292,7 +293,7 @@ size_t compute_locations () {
}
}
#ifdef DEBUG_VERSION
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr, "GC compute_locations finished\n");
#endif
// it will return number of words
@ -300,7 +301,7 @@ size_t compute_locations () {
}
void scan_and_fix_region (memory_chunk *old_heap, void *start, void *end) {
#ifdef DEBUG_VERSION
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr, "GC scan_and_fix_region started\n");
#endif
for (size_t *ptr = (size_t *)start; ptr < (size_t *)end; ++ptr) {
@ -316,14 +317,14 @@ void scan_and_fix_region (memory_chunk *old_heap, void *start, void *end) {
*(void **)ptr = new_addr + content_offset;
}
}
#ifdef DEBUG_VERSION
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr, "GC scan_and_fix_region finished\n");
#endif
}
void scan_and_fix_region_roots (memory_chunk *old_heap) {
#ifdef DEBUG_VERSION
fprintf(stderr, "extra roots started: number os extra roots %i\n", extra_roots.current_free);
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr, "extra roots started: number of extra roots %i\n", extra_roots.current_free);
#endif
for (int i = 0; i < extra_roots.current_free; i++) {
size_t *ptr = (size_t *)extra_roots.roots[i];
@ -339,12 +340,14 @@ void scan_and_fix_region_roots (memory_chunk *old_heap) {
) {
#ifdef DEBUG_VERSION
if (is_valid_heap_pointer((size_t *)ptr_value)) {
# ifdef DEBUG_PRINT
fprintf(stderr,
"|\tskip extra root: %p (%p), since it points to Lama's stack top=%p bot=%p\n",
extra_roots.roots[i],
ptr_value,
__gc_stack_top,
__gc_stack_bottom);
(void *)ptr_value,
(void *)__gc_stack_top,
(void *)__gc_stack_bottom);
# endif
}
# ifdef LAMA_ENV
else if ((extra_roots.roots[i] <= (void *)&__stop_custom_data
@ -353,17 +356,19 @@ void scan_and_fix_region_roots (memory_chunk *old_heap) {
stderr,
"|\tskip extra root: %p (%p), since it points to Lama's static area stop=%p start=%p\n",
extra_roots.roots[i],
ptr_value,
(void *)ptr_value,
(void *)&__stop_custom_data,
(void *)&__start_custom_data);
exit(1);
}
# endif
else {
# ifdef DEBUG_PRINT
fprintf(stderr,
"|\tskip extra root: %p (%p): not a valid Lama pointer \n",
extra_roots.roots[i],
ptr_value);
(void *)ptr_value);
# endif
}
#endif
continue;
@ -374,18 +379,22 @@ void scan_and_fix_region_roots (memory_chunk *old_heap) {
(void *)heap.begin + ((void *)get_forward_address(obj_ptr) - (void *)old_heap->begin);
size_t content_offset = get_header_size(get_type_row_ptr(obj_ptr));
*(void **)ptr = new_addr + content_offset;
#ifdef DEBUG_VERSION
fprintf(stderr, "|\textra root (%p) %p -> %p\n", extra_roots.roots[i], ptr_value, *ptr);
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr,
"|\textra root (%p) %p -> %p\n",
extra_roots.roots[i],
(void *)ptr_value,
(void *)*ptr);
#endif
}
}
#ifdef DEBUG_VERSION
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr, "|\textra roots finished\n");
#endif
}
void update_references (memory_chunk *old_heap) {
#ifdef DEBUG_VERSION
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr, "GC update_references started\n");
#endif
heap_iterator it = heap_begin_iterator();
@ -410,9 +419,11 @@ void update_references (memory_chunk *old_heap) {
size_t content_offset = get_header_size(get_type_row_ptr(field_obj_content_addr));
#ifdef DEBUG_VERSION
if (!is_valid_heap_pointer((void *)(new_addr + content_offset))) {
# ifdef DEBUG_PRINT
fprintf(stderr,
"ur: incorrect pointer assignment: on object with id %d",
TO_DATA(get_object_content_ptr(it.current))->id);
# endif
exit(1);
}
#endif
@ -431,13 +442,13 @@ void update_references (memory_chunk *old_heap) {
assert((void *)&__stop_custom_data >= (void *)&__start_custom_data);
scan_and_fix_region(old_heap, (void *)&__start_custom_data, (void *)&__stop_custom_data);
#endif
#ifdef DEBUG_VERSION
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr, "GC update_references finished\n");
#endif
}
void physically_relocate (memory_chunk *old_heap) {
#ifdef DEBUG_VERSION
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr, "GC physically_relocate started\n");
#endif
heap_iterator from_iter = heap_begin_iterator();
@ -455,7 +466,7 @@ void physically_relocate (memory_chunk *old_heap) {
}
from_iter = next_iter;
}
#ifdef DEBUG_VERSION
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr, "GC physically_relocate finished\n");
#endif
}
@ -532,7 +543,7 @@ void scan_global_area (void) {
#endif
extern void gc_test_and_mark_root (size_t **root) {
#ifdef DEBUG_VERSION
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr,
"\troot = %p (%p), stack addresses: [%p, %p)\n",
root,
@ -563,7 +574,7 @@ extern void __init (void) {
extern void __shutdown (void) {
munmap(heap.begin, heap.size);
#ifdef FULL_INVARIANT_CHECKS
#ifdef DEBUG_VERSION
cur_id = 0;
#endif
heap.begin = NULL;
@ -600,7 +611,7 @@ void pop_extra_root (void **p) {
/* Functions for tests */
#if defined(FULL_INVARIANT_CHECKS) && defined(DEBUG_VERSION)
#if defined(DEBUG_VERSION)
size_t objects_snapshot (int *object_ids_buf, size_t object_ids_buf_size) {
size_t *ids_ptr = (size_t *)object_ids_buf;
size_t i = 0;
@ -720,11 +731,12 @@ lama_type get_type_header_ptr (void *ptr) {
case CLOSURE_TAG: return CLOSURE;
case SEXP_TAG: return SEXP;
default: {
#ifdef DEBUG_VERSION
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr, "ERROR: get_type_header_ptr: unknown object header, cur_id=%d", cur_id);
raise(SIGINT); // only for debug purposes
#else
# ifdef FULL_INVARIANT_CHECKS
# ifdef DEBUG_PRINT
fprintf(stderr,
"ERROR: get_type_header_ptr: unknown object header, ptr is %p, tag %i, heap size is "
"%d cur_id=%d stack_top=%p stack_bot=%p ",
@ -734,6 +746,7 @@ lama_type get_type_header_ptr (void *ptr) {
cur_id,
(void *)__gc_stack_top,
(void *)__gc_stack_bottom);
# endif
FILE *heap_before_compaction = print_objects_traversal("dump_kill", 1);
fclose(heap_before_compaction);
# endif
@ -847,10 +860,10 @@ size_t get_header_size (lama_type type) {
void *alloc_string (int len) {
data *obj = alloc(string_size(len));
obj->data_header = STRING_TAG | (len << 3);
#ifdef DEBUG_VERSION
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr, "%p, [STRING] tag=%zu\n", obj, TAG(obj->data_header));
#endif
#ifdef FULL_INVARIANT_CHECKS
#ifdef DEBUG_VERSION
obj->id = cur_id;
#endif
obj->forward_address = 0;
@ -860,10 +873,10 @@ void *alloc_string (int len) {
void *alloc_array (int len) {
data *obj = alloc(array_size(len));
obj->data_header = ARRAY_TAG | (len << 3);
#ifdef DEBUG_VERSION
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr, "%p, [ARRAY] tag=%zu\n", obj, TAG(obj->data_header));
#endif
#ifdef FULL_INVARIANT_CHECKS
#ifdef DEBUG_VERSION
obj->id = cur_id;
#endif
obj->forward_address = 0;
@ -873,10 +886,10 @@ void *alloc_array (int len) {
void *alloc_sexp (int members) {
sexp *obj = alloc(sexp_size(members));
obj->data_header = SEXP_TAG | (members << 3);
#ifdef DEBUG_VERSION
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr, "%p, SEXP tag=%zu\n", obj, TAG(obj->data_header));
#endif
#ifdef FULL_INVARIANT_CHECKS
#ifdef DEBUG_VERSION
obj->id = cur_id;
#endif
obj->forward_address = 0;
@ -888,10 +901,10 @@ void *alloc_closure (int captured) {
data *obj = alloc(closure_size(captured));
obj->data_header = CLOSURE_TAG | (captured << 3);
#ifdef DEBUG_VERSION
#if defined(DEBUG_VERSION) && defined(DEBUG_PRINT)
fprintf(stderr, "%p, [CLOSURE] tag=%zu\n", obj, TAG(obj->data_header));
#endif
#ifdef FULL_INVARIANT_CHECKS
#ifdef DEBUG_VERSION
obj->id = cur_id;
#endif
obj->forward_address = 0;

View file

@ -46,6 +46,7 @@ typedef struct {
size_t size;
} memory_chunk;
// the only GC-related function that should be exposed, others are useful for tests and internal implementation
// allocates object of the given size on the heap
void *alloc(size_t);
@ -72,6 +73,7 @@ size_t compute_locations ();
void update_references (memory_chunk *);
void physically_relocate (memory_chunk *);
// ============================================================================
// GC extra roots
// ============================================================================
@ -92,26 +94,31 @@ void clear_extra_roots (void);
void push_extra_root (void **p);
void pop_extra_root (void **p);
// ============================================================================
// Implemented in GASM: see gc_runtime.s
// ============================================================================
// MANDATORY TO CALL BEFORE ANY INTERACTION WITH GC (apart from cases where we
// are working with virtual stack as happens in tests)
extern void __gc_init (void);
// should be called before interaction with GC in case of using in tests with
// virtual stack, otherwise it is automatically invoked by `__gc_init`
extern void __init (void);
// mostly useful for tests but basically you want to call this in case you want
// to deallocate all object allocated via GC
extern void __shutdown (void);
// Next two functions sets and unsets `__gc_stack_top`
// The first (`__pre_gc`) should be called in the very beginning of any runtime
// function during the execution of which garbage collection can be initiated.
// The last one is a `companion function` which has to be called at the very
// end of any function that called `__prec_gc`
// end of any function that previously called `__pre_gc`
extern void __pre_gc (void);
extern void __post_gc (void);
// ============================================================================
// invoked from GASM: see gc_runtime.s
// ============================================================================
@ -119,10 +126,11 @@ extern void gc_test_and_mark_root (size_t **root);
bool is_valid_heap_pointer (const size_t *);
static inline bool is_valid_pointer (const size_t *);
// ============================================================================
// Auxiliary functions for tests
// ============================================================================
#if defined(FULL_INVARIANT_CHECKS) && defined(DEBUG_VERSION)
#if defined(DEBUG_VERSION)
// makes a snapshot of current objects in heap (both alive and dead), writes these ids to object_ids_buf,
// returns number of ids dumped
// object_ids_buf is pointer to area preallocated by user for dumping ids of objects in heap
@ -130,6 +138,7 @@ static inline bool is_valid_pointer (const size_t *);
size_t objects_snapshot (int *object_ids_buf, size_t object_ids_buf_size);
#endif
#ifdef DEBUG_VERSION
// essential function to mock program stack
void set_stack (size_t stack_top, size_t stack_bottom);
@ -138,6 +147,7 @@ void set_stack (size_t stack_top, size_t stack_bottom);
void set_extra_roots (size_t extra_roots_size, void **extra_roots_ptr);
#endif
// ============================================================================
// Utility functions
// ============================================================================
@ -186,13 +196,16 @@ size_t obj_size_header_ptr (void *ptr);
// returns total padding size that we need to store given object type
size_t get_header_size (lama_type type);
// returns number of bytes that are required to allocate array with 'sz' elements (header included)
size_t array_size (size_t sz);
// returns number of bytes that are required to allocate string of length 'l' (header included)
size_t string_size (size_t len);
// TODO: ask if it is actually so? number of captured elements is actually sz-1 and 1 extra word is code ptr?
// returns number of bytes that are required to allocate closure with 'sz-1' captured values (header included)
size_t closure_size (size_t sz);
// returns number of bytes that are required to allocate s-expression with 'members' fields (header included)
size_t sexp_size (size_t members);
@ -200,16 +213,21 @@ size_t sexp_size (size_t members);
// (in case of s-exp, it is mandatory that obj ptr is very beginning of the object,
// considering that now we store two versions of header in there)
obj_field_iterator field_begin_iterator (void *obj);
// returns an iterator over object fields which are actual pointers, obj is ptr to object header
// (in case of s-exp, it is mandatory that obj ptr is very beginning of the object,
// considering that now we store two versions of header in there)
obj_field_iterator ptr_field_begin_iterator (void *obj);
// moves the iterator to next object field
void obj_next_field_iterator (obj_field_iterator *it);
// moves the iterator to the next object field which is an actual pointer
void obj_next_ptr_field_iterator (obj_field_iterator *it);
// returns if we are done iterating over fields of the object
bool field_is_done_iterator (obj_field_iterator *it);
// ptr is pointer to the actual object content, returns pointer to the very beginning of the object (header)
void *get_obj_header_ptr (void *ptr);
void *get_object_content_ptr (void *header_ptr);

View file

@ -17,7 +17,7 @@
#define SEXP_ONLY_HEADER_SZ (sizeof(int))
#ifndef FULL_INVARIANT_CHECKS
#ifndef DEBUG_VERSION
# define DATA_HEADER_SZ (sizeof(size_t) + sizeof(int))
#else
# define DATA_HEADER_SZ (sizeof(size_t) + sizeof(size_t) + sizeof(int))
@ -44,7 +44,7 @@ typedef struct {
// other utility info (i.e., size for array, number of fields for s-expression)
int data_header;
#ifdef FULL_INVARIANT_CHECKS
#ifdef DEBUG_VERSION
size_t id;
#endif
@ -59,7 +59,7 @@ typedef struct {
// other utility info (i.e., size for array, number of fields for s-expression)
int data_header;
#ifdef FULL_INVARIANT_CHECKS
#ifdef DEBUG_VERSION
size_t id;
#endif

View file

@ -22,15 +22,15 @@ void test_correct_structure_sizes (void) {
// something like induction base
assert((array_size(0) == get_header_size(ARRAY)));
assert((string_size(0) == get_header_size(STRING) + 1)); // +1 is because of '\0'
assert((sexp_size(0) == get_header_size(SEXP)));
assert((sexp_size(0) == get_header_size(SEXP) + MEMBER_SIZE));
assert((closure_size(0) == get_header_size(CLOSURE)));
// just check correctness for some small sizes
for (int k = 1; k < 20; ++k) {
assert((array_size(k) == get_header_size(ARRAY) + sizeof(int) * k));
assert((array_size(k) == get_header_size(ARRAY) + MEMBER_SIZE * k));
assert((string_size(k) == get_header_size(STRING) + k + 1));
assert((sexp_size(k) == get_header_size(SEXP) + sizeof(int) * k));
assert((closure_size(k) == get_header_size(CLOSURE) + sizeof(int) * k));
assert((sexp_size(k) == get_header_size(SEXP) + MEMBER_SIZE * (k + 1)));
assert((closure_size(k) == get_header_size(CLOSURE) + MEMBER_SIZE * k));
}
}