// Test suite for arena.jai and arena_platform.jai. // // module.jai #loads this file when Basic is imported with LOAD_TESTS = true, so it and the // module are one compilation unit and the #scope_module machinery (Arena_State, // get_arena_state, restore_arena_state) is in reach. It is still exercised through the // public wrappers on purpose -- arena_release and the auto_* macros -- because the public // contract is what callers depend on. // // Covers every exported symbol of both files. ARENA_DEBUG is Basic's own module parameter // here, so the suite tests the module as it was actually built. // // Groups are toggled with the RUN_* constants below. Each group is #if'd at its call site // in run_arena_tests(), so a disabled group is not invoked at all. // // Several groups deliberately drive failure paths that log. Expected log_error and // log_warning output is bracketed by "expecting errors" banners -- noise inside a // banner is the test working, noise outside one is not. // --------------------------------------------------------------------------- // Group switches // --------------------------------------------------------------------------- RUN_LIFECYCLE_TESTS :: true; // arena_init, bootstrap_arena, arena_delete, is_valid RUN_INTROSPECTION_TESTS :: true; // arena_start/limit/usage/committed, reserve_size_bytes, next_reserve RUN_ALLOCATION_TESTS :: true; // arena_alloc happy paths, commit growth RUN_BOUNDS_TESTS :: true; // negative sizes, exhaustion, address-limit refusal RUN_ALIGNMENT_TESTS :: true; // arena_set_alignment, push_alignment, Align RUN_RESET_TESTS :: true; // arena_reset, arena_reset_keeping_memory, arena_release RUN_SCOPED_TESTS :: true; // auto_reset, auto_release, push_arena, the overwriting variants RUN_ALLOCATOR_TESTS :: true; // arena_allocator_proc modes, get_allocator, get_capabilities RUN_SECURE_TESTS :: true; // arena_wipe, arena_secure_overwrite[_starting_from] RUN_PLATFORM_TESTS :: true; // arena_platform_init, extend_committed_pages, commit/lock/unlock // Paths whose preconditions are enforced by assert() rather than by a runtime check. // Running these with ENABLE_ASSERT on will abort the process, which is the assert doing // its job -- so they are off by default. Turn on only in a build with ENABLE_ASSERT=false, // to confirm the release-mode fallbacks (log and return) behave. RUN_ASSERT_TRIP_TESTS :: false; VERBOSE :: false; // print each arena's details as groups run // --------------------------------------------------------------------------- // Harness // --------------------------------------------------------------------------- #scope_file // Per-group tallies. Fixed size and allocation-free on purpose: expect() is called // from inside push_arena scopes, so anything it allocated would come out of the arena // under test and be freed the moment that arena is reset or deleted. MAX_GROUPS :: 16; group_names : [MAX_GROUPS] string; group_checks : [MAX_GROUPS] int; group_fails : [MAX_GROUPS] int; group_count := 0; checks_run := 0; checks_failed := 0; // Set by run_arena_tests. One suite is one work item, so a file-scope pointer is enough; // null means write straight to stdout, as a synchronous or standalone run does. output_builder: *String_Builder; emit :: (format_string: string, args: ..Any) { if output_builder print_to_builder(output_builder, format_string, ..args); else print(format_string, ..args); } @PrintLike group :: (name: string) { assert(group_count < MAX_GROUPS, "raise MAX_GROUPS"); group_names[group_count] = name; group_count += 1; emit("\n-- % --\n", name); } expect :: (condition: bool, format: string, args: ..Any, loc := #caller_location) { checks_run += 1; if group_count group_checks[group_count-1] += 1; if condition return; checks_failed += 1; if group_count group_fails[group_count-1] += 1; name := ifx group_count then group_names[group_count-1] else "?"; emit(" FAIL [%:%] %\n", name, loc.line_number, tprint(format, ..args)); } expecting_errors :: (what: string) { emit(" (expecting logged errors: %)\n", what); } // A fresh, non-bootstrapped arena backed by its own reservation. // The Arena struct is POD and, for a non-bootstrapped arena, holds no pointers into // itself, so returning it by value is safe. make_arena :: (reserve := Arena_Reserve.Size_2M, pages := ARENA_DEFAULT_COMMIT_PAGE_COUNT) -> Arena { a: Arena; success, committed := arena_init(*a, reserve, pages); expect(success, "arena_init(%, % pages) reported failure", reserve, pages); expect(committed > 0, "arena_init(%, % pages) committed nothing", reserve, pages); return a; } fill :: (memory: *void, count: s64, value: u8) { bytes := cast(*u8) memory; for 0..count-1 bytes[it] = value; } all_bytes_are :: (memory: *void, count: s64, value: u8) -> bool { bytes := cast(*u8) memory; for 0..count-1 if bytes[it] != value return false; return true; } is_aligned :: (pointer: *void, alignment: s64) -> bool { return (cast(s64) pointer) % alignment == 0; } DEFAULT_COMMIT_BYTES :: ARENA_DEFAULT_COMMIT_PAGE_COUNT * PLATFORM_MEMORY_PAGE_SIZE; // --------------------------------------------------------------------------- // Lifecycle: arena_init, bootstrap_arena, arena_delete, is_valid // --------------------------------------------------------------------------- lifecycle_tests :: () { group("lifecycle"); // A zeroed Arena is not valid until it is initialized. { a: Arena; expect(!is_valid(*a), "a zeroed Arena reported valid"); expect(a.memory_base == null, "a zeroed Arena has a non-null memory_base"); } // Initialization establishes every invariant the rest of the module depends on. { a := make_arena(.Size_2M); defer arena_delete(*a); expect(is_valid(*a), "arena is not valid after arena_init"); expect(a.memory_base != null, "memory_base is null after a successful arena_init"); expect(a.reserve == .Size_2M, "reserve was not recorded; got %", a.reserve); expect(arena_alignment(*a) == ARENA_DEFAULT_ALIGNMENT, "alignment is % not the default %", arena_alignment(*a), ARENA_DEFAULT_ALIGNMENT); expect(a.current_point == arena_start(*a), "current_point does not start at arena_start"); expect(arena_usage_bytes(*a) == 0, "a fresh arena reports % bytes used", arena_usage_bytes(*a)); expect(arena_committed_bytes(*a) == DEFAULT_COMMIT_BYTES, "default commit is % bytes, expected %", arena_committed_bytes(*a), DEFAULT_COMMIT_BYTES); expect(arena_initial_commit_pages(*a) == ARENA_DEFAULT_COMMIT_PAGE_COUNT, "the initial commit is % pages not %", arena_initial_commit_pages(*a), ARENA_DEFAULT_COMMIT_PAGE_COUNT); expect(!arena_is_bootstrapped(*a), "a stack-held arena claims to be bootstrapped"); } // Re-initializing an already-live arena is a no-op that still reports success, // and must not replace the existing reservation. { a := make_arena(.Size_2M); defer arena_delete(*a); base_before := a.memory_base; arena_alloc(*a, 128); used_before := arena_usage_bytes(*a); success, pages := arena_init(*a, .Size_64M); expect(success, "re-initializing a live arena reported failure"); expect(a.memory_base == base_before, "re-initializing replaced memory_base"); expect(a.reserve == .Size_2M, "re-initializing changed the reserve class to %", a.reserve); expect(pages == arena_initial_commit_pages(*a), "re-init reported % pages, field holds %", pages, arena_initial_commit_pages(*a)); expect(arena_usage_bytes(*a) == used_before, "re-initializing discarded % bytes of live allocation", used_before); } // A commit request of zero is promoted to the default rather than producing an // arena with no backing pages. Regression guard for C4. { a := make_arena(.Size_2M, 0); defer arena_delete(*a); expect(arena_initial_commit_pages(*a) >= 1, "commit_page_count = 0 recorded % pages; a bootstrapped arena needs at least 1 for its header", arena_initial_commit_pages(*a)); expect(arena_committed_bytes(*a) >= PLATFORM_MEMORY_PAGE_SIZE, "commit_page_count = 0 left % bytes committed", arena_committed_bytes(*a)); expect(arena_committed_bytes(*a) == arena_initial_commit_pages(*a) * PLATFORM_MEMORY_PAGE_SIZE, "recorded page count % disagrees with % bytes actually committed", arena_initial_commit_pages(*a), arena_committed_bytes(*a)); } // A negative commit request is clamped to the default. The stored count is a log2, so an // unclamped negative would reach log2_of_power_of_two rather than becoming a huge page count. { expecting_errors("negative commit_page_count"); a := make_arena(.Size_2M, -4); defer arena_delete(*a); expect(arena_initial_commit_pages(*a) == ARENA_DEFAULT_COMMIT_PAGE_COUNT, "negative commit_page_count recorded % pages, expected the default %", arena_initial_commit_pages(*a), ARENA_DEFAULT_COMMIT_PAGE_COUNT); expect(arena_committed_bytes(*a) == arena_initial_commit_pages(*a) * PLATFORM_MEMORY_PAGE_SIZE, "recorded page count disagrees with bytes committed after a negative request"); } // A count that is not a power of two but does fit its reserve is rounded up, because the // arena stores the count as a log2. Rounding up rather than down keeps the initial commit at // least what was asked for, and keeps the stored value exact rather than approximate. // The .Size_64K case below does not exercise this: 20 pages already exceeds a 64K reserve, so // it takes the clamp instead and lands on 16 either way. { ROUNDED :: 32; // 20 rounded up a := make_arena(.Size_64M, 20); defer arena_delete(*a); expect(arena_initial_commit_pages(*a) == ROUNDED, "a 20 page request recorded % pages, expected % after rounding up to a power of two", arena_initial_commit_pages(*a), ROUNDED); expect(arena_committed_bytes(*a) == ROUNDED * PLATFORM_MEMORY_PAGE_SIZE, "a 20 page request committed % bytes, expected %", arena_committed_bytes(*a), ROUNDED * PLATFORM_MEMORY_PAGE_SIZE); } // Asking for more initial commit than the reserve class holds clamps to the reserve. { expecting_errors("commit larger than the 64K reserve"); a := make_arena(.Size_64K, 20); defer arena_delete(*a); expect(is_valid(*a), "an over-committed 64K arena did not initialize"); expect(arena_committed_bytes(*a) <= reserve_size_bytes(*a), "committed % exceeds the % byte reserve", arena_committed_bytes(*a), reserve_size_bytes(*a)); expect(arena_committed_bytes(*a) == reserve_size_bytes(*a), "expected the clamp to commit the whole 64K reserve, got %", arena_committed_bytes(*a)); } // arena_delete releases a non-bootstrapped arena and leaves the struct reusable. { a := make_arena(.Size_2M); arena_delete(*a); expect(!is_valid(*a), "arena is still valid after arena_delete"); expect(a.memory_base == null, "arena_delete left a non-null memory_base"); // The struct can be initialized again afterwards. success, _ := arena_init(*a, .Size_2M); expect(success, "could not re-initialize an arena after deleting it"); expect(is_valid(*a), "re-initialized arena is not valid"); arena_delete(*a); } // arena_delete tolerates an arena that was never initialized. { a: Arena; arena_delete(*a); expect(!is_valid(*a), "deleting an uninitialized arena made it valid"); } // Bootstrapping puts the header inside the arena's own first page. { a := bootstrap_arena(.Size_2M, label = "bootstrap-lifecycle"); expect(a != null, "bootstrap_arena returned null"); if a { expect(is_valid(a), "bootstrapped arena is not valid"); expect(arena_is_bootstrapped(a), "bootstrapped arena does not carry Is_Bootstrapped"); expect(cast(*void) a == cast(*void) a.memory_base, "the header does not sit at memory_base"); expect(a.current_point == arena_start(a), "a fresh bootstrapped arena is not empty"); expect(arena_usage_bytes(a) == 0, "a fresh bootstrapped arena reports % bytes used", arena_usage_bytes(a)); // Allocating from it must not tread on the header. p := arena_alloc(a, 64); expect(p != null, "allocation from a bootstrapped arena returned null"); expect(cast(*u8) p >= arena_start(a), "allocation overlaps the bootstrapped header"); expect(a.memory_base != null, "the header was corrupted by an allocation"); // Nothing may touch `a` after this: the struct lives in the memory being released. arena_delete(a); } } // Bootstrapping with a zero commit request still yields a usable arena. Regression guard for C4. { a := bootstrap_arena(.Size_2M, 0, label = "bootstrap-zero-commit"); expect(a != null, "bootstrap_arena(.Size_2M, 0) returned null"); if a { expect(arena_initial_commit_pages(a) >= 1, "bootstrapped arena recorded % initial pages", arena_initial_commit_pages(a)); p := arena_alloc(a, 32); expect(p != null, "could not allocate from a zero-commit bootstrapped arena"); arena_delete(a); } } // The clamp path must produce a working bootstrapped arena, not a failure. Regression guard for H1. { expecting_errors("bootstrap commit larger than the 64K reserve"); a := bootstrap_arena(.Size_64K, 20, label = "bootstrap-clamped"); expect(a != null, "bootstrap_arena(.Size_64K, 20) returned null; the clamp should have made this work"); if a { expect(arena_committed_bytes(a) == reserve_size_bytes(a), "clamped bootstrap committed % of %", arena_committed_bytes(a), reserve_size_bytes(a)); arena_delete(a); } } } // --------------------------------------------------------------------------- // Introspection: layout accessors, reserve table, next_reserve // --------------------------------------------------------------------------- introspection_tests :: () { group("introspection"); // The reserve table and the enum must stay in step. #run asserts this at compile // time; this checks the values themselves. { expect(reserve_size_bytes(Arena_Reserve.Size_64K) == 64 * 1024, "Size_64K is %", reserve_size_bytes(Arena_Reserve.Size_64K)); expect(reserve_size_bytes(Arena_Reserve.Size_2M) == 2 * 1024 * 1024, "Size_2M is %", reserve_size_bytes(Arena_Reserve.Size_2M)); expect(reserve_size_bytes(Arena_Reserve.Size_64M) == 64 * 1024 * 1024, "Size_64M is %", reserve_size_bytes(Arena_Reserve.Size_64M)); expect(reserve_size_bytes(Arena_Reserve.Size_2G) == 2 * 1024 * 1024 * 1024, "Size_2G is %", reserve_size_bytes(Arena_Reserve.Size_2G)); // Monotonically increasing, so next_reserve's linear scan is meaningful. for i: 1..Arena_Sizes.count-1 { expect(Arena_Sizes[i] > Arena_Sizes[i-1], "Arena_Sizes is not increasing at index %", i); } } // next_reserve must account for the header, or it names a class that cannot hold // the request. Regression guard for L2. { expect(next_reserve(1024, 8) == .Size_64K, "next_reserve(1024) is %", next_reserve(1024, 8)); // Exactly the size of the smallest class does not fit in it, because the header // is carved out of the front. expect(next_reserve(64 * 1024, 8) == .Size_2M, "next_reserve(64K) is % -- a 64K class cannot hold 64K once the header is taken out", next_reserve(64 * 1024, 8)); // Comfortably under the limit still fits. expect(next_reserve(64 * 1024 - size_of(Arena) - 64, 8) == .Size_64K, "next_reserve(just under 64K) is %", next_reserve(64 * 1024 - size_of(Arena) - 64, 8)); // Beyond the largest class, saturate rather than run off the table. expect(next_reserve(Arena_Sizes[Arena_Sizes.count-1], 8) == .Size_64T, "next_reserve(over the largest class) is %", next_reserve(Arena_Sizes[Arena_Sizes.count-1], 8)); } // Layout accessors agree with each other and with the reserve class. { a := make_arena(.Size_2M); defer arena_delete(*a); expect(arena_start(*a) == Align(a.memory_base + size_of(Arena), ARENA_DEFAULT_ALIGNMENT), "arena_start does not skip the header"); expect(arena_start(*a) > a.memory_base, "arena_start is not past memory_base"); expect(is_aligned(arena_start(*a), ARENA_DEFAULT_ALIGNMENT), "arena_start is not aligned"); expect(arena_address_limit(*a) == a.memory_base + reserve_size_bytes(*a), "arena_address_limit is wrong"); expect(reserve_size_bytes(*a) == reserve_size_bytes(Arena_Reserve.Size_2M), "the two reserve_size_bytes overloads disagree"); expect(arena_usage_bytes(*a) == 0, "fresh usage is %", arena_usage_bytes(*a)); expect(arena_committed_bytes(*a) == cast(s64)(a.first_uncommitted_page - a.memory_base), "arena_committed_bytes is wrong"); arena_alloc(*a, 1000); expect(arena_usage_bytes(*a) == cast(s64)(a.current_point - arena_start(*a)), "arena_usage_bytes is wrong after allocating"); expect(arena_usage_bytes(*a) >= 1000, "usage % is under the 1000 bytes allocated", arena_usage_bytes(*a)); expect(arena_committed_bytes(*a) <= reserve_size_bytes(*a), "committed exceeds the reserve"); expect(a.current_point <= a.first_uncommitted_page, "current_point is past the committed region"); expect(a.first_uncommitted_page <= arena_address_limit(*a), "first_uncommitted_page is past the address limit"); } // The bootstrap flag helpers. { a := make_arena(.Size_2M); defer arena_delete(*a); expect(!arena_is_bootstrapped(*a), "a fresh arena claims to be bootstrapped"); arena_set_bootstrap_flag(*a); expect(arena_is_bootstrapped(*a), "arena_set_bootstrap_flag did not take"); expect(a.flags & .Is_Bootstrapped == .Is_Bootstrapped, "the flag bit is not set"); // Clear it again so the deferred arena_delete frees the reservation and nulls the // base, rather than taking the bootstrapped path. a.flags = .None; } // arena_details is a debugging aid; it must survive a null and an uninitialized arena. { null_text := arena_details(null); expect(null_text.count > 0, "arena_details(null) returned an empty string"); blank: Arena; blank_text := arena_details(*blank); expect(blank_text.count > 0, "arena_details on an uninitialized arena returned an empty string"); a := make_arena(.Size_2M); defer arena_delete(*a); arena_alloc(*a, 4096); live_text := arena_details(*a); expect(live_text.count > 0, "arena_details on a live arena returned an empty string"); #if VERBOSE { emit(" %", null_text); emit(" %", live_text); } } // The struct footprint is a deliberate design constraint: the header is carved out // of every arena's first page, so growing it costs usable space in every arena. // If this fires, it is a decision to make, not a number to update blindly. // // Three pointers and six single-byte fields is 30 bytes, rounding to 32 at pointer alignment, // so two bytes are spare. That is what the log2 fields buy: alignment, page size and the // initial commit count each fit in a byte. One field wider than a byte would push this to 40. #if !ARENA_DEBUG { expect(size_of(Arena) == 32, "size_of(Arena) is % not 32 -- a field changed width; confirm the footprint cost is intended", size_of(Arena)); } } // --------------------------------------------------------------------------- // Allocation: arena_alloc happy paths and on-demand commit // --------------------------------------------------------------------------- allocation_tests :: () { group("allocation"); // Basic allocation lands inside the arena and is writable. { a := make_arena(.Size_2M); defer arena_delete(*a); p := arena_alloc(*a, 256); expect(p != null, "arena_alloc returned null for a 256 byte request"); expect(cast(*u8) p >= arena_start(*a), "allocation is below arena_start"); expect(cast(*u8) p + 256 <= a.first_uncommitted_page, "allocation extends past the committed region"); fill(p, 256, 0xAB); expect(all_bytes_are(p, 256, 0xAB), "could not read back what was written to the allocation"); } // Successive allocations do not overlap and advance monotonically. { a := make_arena(.Size_2M); defer arena_delete(*a); SIZES :: s64.[1, 7, 8, 9, 64, 100, 4096, 3]; previous_end: *u8 = arena_start(*a); for size: SIZES { p := cast(*u8) arena_alloc(*a, size); expect(p != null, "arena_alloc(%) returned null", size); expect(p >= previous_end, "allocation of % overlaps the previous one", size); expect(is_aligned(p, arena_alignment(*a)), "allocation of % is not aligned to %", size, arena_alignment(*a)); fill(p, size, 0x5A); previous_end = p + size; } // Every region still holds its own pattern, i.e. nothing was handed out twice. expect(a.current_point >= previous_end, "current_point is behind the last allocation"); } // A zero-byte request is legal and does not advance the arena. { a := make_arena(.Size_2M); defer arena_delete(*a); arena_alloc(*a, 24); // get off the aligned base before := a.current_point; p := arena_alloc(*a, 0); expect(p != null, "a zero-byte allocation returned null"); expect(a.current_point == before || arena_usage_bytes(*a) >= 0, "a zero-byte allocation moved current_point backwards"); expect(a.current_point >= before, "a zero-byte allocation moved current_point backwards"); } // Crossing the committed boundary commits more pages rather than failing. { a := make_arena(.Size_2M); defer arena_delete(*a); committed_before := arena_committed_bytes(*a); expect(committed_before == DEFAULT_COMMIT_BYTES, "unexpected starting commit of %", committed_before); // Ask for more than is currently committed. size := committed_before + 4096; p := arena_alloc(*a, size); expect(p != null, "allocation across the commit boundary returned null"); expect(arena_committed_bytes(*a) > committed_before, "the committed region did not grow"); expect(arena_committed_bytes(*a) % PLATFORM_MEMORY_PAGE_SIZE == 0, "committed size % is not page aligned", arena_committed_bytes(*a)); expect(cast(*u8) p + size <= a.first_uncommitted_page, "the allocation is not fully committed"); // The whole span must be writable, which is the real test of the commit. fill(p, size, 0xC3); expect(all_bytes_are(p, size, 0xC3), "the freshly committed span is not readable/writable"); } // Many small allocations in sequence, spanning several page commits. { a := make_arena(.Size_2M); defer arena_delete(*a); // A stress run is one assertion, not COUNT of them. Checking inside the loop would // report two thousand passing checks for a single property and drown out the rest of // the suite in the totals, so failures are counted and reported once. COUNT :: 2000; CHUNK :: 64; // a multiple of the default alignment, so the blocks come out contiguous first: *u8; null_returns := 0; for 0..COUNT-1 { p := cast(*u8) arena_alloc(*a, CHUNK); if !p { null_returns += 1; continue; } if it == 0 first = p; fill(p, CHUNK, cast(u8)(it % 256)); } expect(null_returns == 0, "% of % stress allocations returned null", null_returns, COUNT); expect(first != null, "the stress loop never recorded its first allocation"); expect(arena_usage_bytes(*a) >= COUNT * CHUNK, "usage % is under the % bytes handed out", arena_usage_bytes(*a), COUNT * CHUNK); expect(a.current_point <= a.first_uncommitted_page, "current_point ran past the committed region"); // Every block still holds its own pattern, which is the real property under test: // a bump allocator must never hand the same bytes out twice, across page commits // included. CHUNK is a multiple of the alignment, so block N sits at first + N*CHUNK. if first { clobbered := 0; for 0..COUNT-1 { if !all_bytes_are(first + it * CHUNK, CHUNK, cast(u8)(it % 256)) clobbered += 1; } expect(clobbered == 0, "% of % stress blocks were overwritten; allocations overlapped", clobbered, COUNT); } } // The high-water mark tracks allocations made through arena_alloc directly, not // only those made through the allocator proc. Regression guard for L4. #if ARENA_DEBUG { a := make_arena(.Size_2M); defer arena_delete(*a); arena_alloc(*a, 5000); peak := a.high_water_mark; expect(peak >= 5000, "high_water_mark is % after allocating 5000 bytes directly", peak); arena_reset(*a); arena_alloc(*a, 100); expect(a.high_water_mark == peak, "high_water_mark fell to % after a reset; it should hold the peak", a.high_water_mark); } } // --------------------------------------------------------------------------- // Bounds: refusals that must return null without corrupting the arena // --------------------------------------------------------------------------- bounds_tests :: () { group("bounds"); // A negative size is refused and leaves the arena untouched. Regression guard for M8. { expecting_errors("negative allocation size"); a := make_arena(.Size_2M); defer arena_delete(*a); arena_alloc(*a, 128); point_before := a.current_point; usage_before := arena_usage_bytes(*a); p := arena_alloc(*a, -1); expect(p == null, "a negative allocation returned a pointer"); expect(a.current_point == point_before, "a negative allocation moved current_point"); expect(arena_usage_bytes(*a) == usage_before, "a negative allocation changed usage"); p2 := arena_alloc(*a, -1024 * 1024); expect(p2 == null, "a large negative allocation returned a pointer"); expect(a.current_point == point_before, "a large negative allocation moved current_point"); // The arena still works afterwards. p3 := arena_alloc(*a, 64); expect(p3 != null, "the arena stopped working after refusing a negative size"); } // A request larger than the whole reserve is refused. Regression guard for C3. { expecting_errors("allocation larger than the reserve"); a := make_arena(.Size_64K); defer arena_delete(*a); point_before := a.current_point; p := arena_alloc(*a, reserve_size_bytes(*a) + 1); expect(p == null, "an over-reserve allocation returned a pointer"); expect(a.current_point == point_before, "an over-reserve allocation moved current_point"); expect(a.current_point <= arena_address_limit(*a), "current_point escaped the reservation"); } // A size near S64_MAX must not wrap the pointer arithmetic into something that // looks in range. Regression guard for M8. { expecting_errors("absurd allocation size"); a := make_arena(.Size_2M); defer arena_delete(*a); point_before := a.current_point; p := arena_alloc(*a, 0x7FFF_FFFF_FFFF_FFFF); expect(p == null, "an S64_MAX allocation returned a pointer"); expect(a.current_point == point_before, "an S64_MAX allocation moved current_point"); p2 := arena_alloc(*a, 0x4000_0000_0000_0000); expect(p2 == null, "a 2^62 allocation returned a pointer"); expect(a.current_point == point_before, "a 2^62 allocation moved current_point"); } // Filling the arena exactly, then failing to exceed it. { expecting_errors("exhausting a 64K arena"); a := make_arena(.Size_64K); defer arena_delete(*a); remaining := cast(s64)(arena_address_limit(*a) - a.current_point); expect(remaining > 0, "no space available in a fresh 64K arena"); p := arena_alloc(*a, remaining); expect(p != null, "could not allocate the exact remaining % bytes", remaining); expect(a.current_point == arena_address_limit(*a), "current_point is not at the limit after filling the arena"); q := arena_alloc(*a, 1); expect(q == null, "allocated one byte past a full arena"); expect(a.current_point == arena_address_limit(*a), "current_point moved past the limit"); } // A refusal must not leave the arena in a state where later allocations misbehave. { expecting_errors("refusal followed by recovery"); a := make_arena(.Size_2M); defer arena_delete(*a); arena_alloc(*a, -5); arena_alloc(*a, reserve_size_bytes(*a) * 2); p := cast(*u8) arena_alloc(*a, 4096); expect(p != null, "the arena is unusable after two refusals"); expect(p >= arena_start(*a) && p + 4096 <= a.first_uncommitted_page, "post-refusal allocation is out of bounds"); fill(p, 4096, 0x11); expect(all_bytes_are(p, 4096, 0x11), "post-refusal allocation is not writable"); } } // --------------------------------------------------------------------------- // Alignment: arena_set_alignment, push_alignment, Align // --------------------------------------------------------------------------- alignment_tests :: () { group("alignment"); // Align itself, on integers and pointers. { expect(Align(0, 8) == 0, "Align(0, 8) is %", Align(0, 8)); expect(Align(1, 8) == 8, "Align(1, 8) is %", Align(1, 8)); expect(Align(8, 8) == 8, "Align(8, 8) is %", Align(8, 8)); expect(Align(9, 8) == 16, "Align(9, 8) is %", Align(9, 8)); expect(Align(4095, PLATFORM_MEMORY_PAGE_SIZE) == 4096, "Align(4095, page) is %", Align(4095, PLATFORM_MEMORY_PAGE_SIZE)); expect(Align(4096, PLATFORM_MEMORY_PAGE_SIZE) == 4096, "Align(4096, page) is %", Align(4096, PLATFORM_MEMORY_PAGE_SIZE)); expect(Align(4097, PLATFORM_MEMORY_PAGE_SIZE) == 8192, "Align(4097, page) is %", Align(4097, PLATFORM_MEMORY_PAGE_SIZE)); } // Setting a valid alignment changes what subsequent allocations return. { a := make_arena(.Size_2M); defer arena_delete(*a); ALIGNMENTS :: s64.[8, 16, 32, 64, 256, 4096]; for wanted: ALIGNMENTS { arena_set_alignment(*a, wanted); expect(arena_alignment(*a) == wanted, "alignment is % after asking for %", arena_alignment(*a), wanted); // Push the cursor to an awkward offset first, so the aligner has work to do. arena_alloc(*a, 1); p := arena_alloc(*a, 16); expect(p != null, "allocation returned null at alignment %", wanted); expect(is_aligned(p, wanted), "allocation % is not aligned to %", p, wanted); } } // Invalid alignments are refused and leave the existing value alone. // These are runtime-checked, but the procedure also asserts first, so they only // run in a build with asserts off. #if RUN_ASSERT_TRIP_TESTS { expecting_errors("invalid alignments"); a := make_arena(.Size_2M); defer arena_delete(*a); arena_set_alignment(*a, 16); expect(arena_alignment(*a) == 16, "setup failed; alignment is %", arena_alignment(*a)); arena_set_alignment(*a, 0); expect(arena_alignment(*a) == 16, "an alignment of 0 was accepted; alignment is now %", arena_alignment(*a)); arena_set_alignment(*a, -8); expect(arena_alignment(*a) == 16, "a negative alignment was accepted; alignment is now %", arena_alignment(*a)); arena_set_alignment(*a, 24); expect(arena_alignment(*a) == 16, "a non-power-of-two alignment was accepted; alignment is now %", arena_alignment(*a)); // The bound is the reservation, not a field width: an alignment wider than the whole // reservation would make every allocation fail the address-limit check, and anything at or // below it is representable now that the field holds a log2. arena_set_alignment(*a, 4 * 1024 * 1024); expect(arena_alignment(*a) == reserve_size_bytes(*a), "an alignment wider than the reservation gave %, expected the % byte reservation", arena_alignment(*a), reserve_size_bytes(*a)); } // What the old u16 field could not express. 65536 used to be clamped to 32768, and a 2 MB // alignment was not representable at all; both are exact now. { a := make_arena(.Size_64M); defer arena_delete(*a); for wanted: s64.[65536, 2 * 1024 * 1024] { arena_set_alignment(*a, wanted); expect(arena_alignment(*a) == wanted, "alignment is % after asking for %", arena_alignment(*a), wanted); arena_alloc(*a, 1); p := arena_alloc(*a, 16); expect(p != null, "allocation returned null at alignment %", wanted); expect(is_aligned(p, wanted), "allocation % is not aligned to %", p, wanted); } } // push_alignment applies for the enclosing scope and restores afterwards. { a := make_arena(.Size_2M); defer arena_delete(*a); arena_set_alignment(*a, 8); before := arena_alignment(*a); { push_alignment(*a, 64); expect(arena_alignment(*a) == 64, "push_alignment did not apply; alignment is %", arena_alignment(*a)); arena_alloc(*a, 1); p := arena_alloc(*a, 8); expect(is_aligned(p, 64), "allocation inside push_alignment is not 64 aligned"); } expect(arena_alignment(*a) == before, "push_alignment did not restore; alignment is % not %", arena_alignment(*a), before); arena_alloc(*a, 1); q := arena_alloc(*a, 8); expect(is_aligned(q, before), "allocation after push_alignment is not aligned to the restored %", before); } // Nested pushes unwind in order. { a := make_arena(.Size_2M); defer arena_delete(*a); arena_set_alignment(*a, 8); { push_alignment(*a, 32); { push_alignment(*a, 256); expect(arena_alignment(*a) == 256, "inner push_alignment gave %", arena_alignment(*a)); } expect(arena_alignment(*a) == 32, "after the inner scope alignment is % not 32", arena_alignment(*a)); } expect(arena_alignment(*a) == 8, "after both scopes alignment is % not 8", arena_alignment(*a)); } } // --------------------------------------------------------------------------- // Reset family: arena_reset, arena_reset_keeping_memory, arena_release // --------------------------------------------------------------------------- reset_tests :: () { group("reset"); // arena_reset rewinds the cursor and keeps the pages committed. { a := make_arena(.Size_2M); defer arena_delete(*a); arena_alloc(*a, 200 * 1024); // force the committed region to grow grown := arena_committed_bytes(*a); expect(grown > DEFAULT_COMMIT_BYTES, "the committed region did not grow; it is %", grown); arena_reset(*a); expect(arena_usage_bytes(*a) == 0, "usage is % after arena_reset", arena_usage_bytes(*a)); expect(a.current_point == arena_start(*a), "current_point is not at arena_start after arena_reset"); expect(arena_committed_bytes(*a) == grown, "arena_reset decommitted pages; committed went from % to %", grown, arena_committed_bytes(*a)); // Reusing the arena after a reset hands back the same base address. p := arena_alloc(*a, 64); expect(cast(*u8) p == arena_start(*a), "the first allocation after a reset is not at arena_start"); } // arena_reset_keeping_memory behaves the same way. { a := make_arena(.Size_2M); defer arena_delete(*a); arena_alloc(*a, 200 * 1024); grown := arena_committed_bytes(*a); arena_reset_keeping_memory(*a); expect(arena_usage_bytes(*a) == 0, "usage is % after arena_reset_keeping_memory", arena_usage_bytes(*a)); expect(arena_committed_bytes(*a) == grown, "arena_reset_keeping_memory decommitted pages"); } // arena_release rewinds and gives the pages back, down to the initial commit. { a := make_arena(.Size_2M); defer arena_delete(*a); arena_alloc(*a, 400 * 1024); expect(arena_committed_bytes(*a) > DEFAULT_COMMIT_BYTES, "the committed region did not grow before release"); arena_release(*a); expect(arena_usage_bytes(*a) == 0, "usage is % after arena_release", arena_usage_bytes(*a)); expect(arena_committed_bytes(*a) == DEFAULT_COMMIT_BYTES, "arena_release left % bytes committed, expected the initial %", arena_committed_bytes(*a), DEFAULT_COMMIT_BYTES); expect(a.current_point <= a.first_uncommitted_page, "current_point is past the committed region after release"); // The arena must still work, re-committing on demand. p := arena_alloc(*a, 300 * 1024); expect(p != null, "could not allocate after arena_release"); fill(p, 300 * 1024, 0x77); expect(all_bytes_are(p, 300 * 1024, 0x77), "memory re-committed after release is not writable"); } // Releasing a bootstrapped arena must never decommit the page holding its header. // Regression guard for C4. { a := bootstrap_arena(.Size_2M, label = "release-keeps-header"); expect(a != null, "bootstrap_arena returned null"); if a { arena_alloc(a, 400 * 1024); arena_release(a); // Reading these fields at all is the test: the header page must still be mapped. expect(is_valid(a), "the header did not survive arena_release"); expect(arena_committed_bytes(a) >= PLATFORM_MEMORY_PAGE_SIZE, "release left % bytes committed", arena_committed_bytes(a)); expect(arena_usage_bytes(a) == 0, "usage is % after release", arena_usage_bytes(a)); p := arena_alloc(a, 64); expect(p != null, "a bootstrapped arena is unusable after arena_release"); arena_delete(a); } } // arena_reset_overwriting_memory zeroes what it rewinds past. { a := make_arena(.Size_2M); defer arena_delete(*a); SIZE :: 8192; p := arena_alloc(*a, SIZE); fill(p, SIZE, 0xDD); expect(all_bytes_are(p, SIZE, 0xDD), "setup failed; the pattern did not stick"); arena_reset_overwriting_memory(*a); expect(arena_usage_bytes(*a) == 0, "usage is % after the overwriting reset", arena_usage_bytes(*a)); expect(all_bytes_are(p, SIZE, 0), "the overwriting reset left % bytes unwiped -- it did nothing", SIZE); } // The reset family is a no-op on an uninitialized arena rather than a crash. { blank: Arena; arena_reset(*blank); arena_reset_keeping_memory(*blank); arena_release(*blank); arena_reset_overwriting_memory(*blank); expect(!is_valid(*blank), "resetting an uninitialized arena made it valid"); } } // --------------------------------------------------------------------------- // Scoped helpers: auto_reset, auto_release, push_arena, overwriting variants // --------------------------------------------------------------------------- scoped_tests :: () { group("scoped"); // auto_reset restores the cursor at scope exit and keeps the pages. { a := make_arena(.Size_2M); defer arena_delete(*a); arena_alloc(*a, 1024); usage_before := arena_usage_bytes(*a); point_before := a.current_point; { auto_reset(*a); arena_alloc(*a, 200 * 1024); expect(arena_usage_bytes(*a) > usage_before, "the inner allocation did not happen"); } expect(a.current_point == point_before, "auto_reset did not restore current_point"); expect(arena_usage_bytes(*a) == usage_before, "usage is % after auto_reset, expected %", arena_usage_bytes(*a), usage_before); expect(arena_committed_bytes(*a) > DEFAULT_COMMIT_BYTES, "auto_reset gave pages back; it should keep them"); } // auto_release restores the cursor and gives the pages back. { a := make_arena(.Size_2M); defer arena_delete(*a); arena_alloc(*a, 512); point_before := a.current_point; { auto_release(*a); arena_alloc(*a, 400 * 1024); expect(arena_committed_bytes(*a) > DEFAULT_COMMIT_BYTES, "the inner allocation did not grow the commit"); } expect(a.current_point == point_before, "auto_release did not restore current_point"); expect(arena_committed_bytes(*a) <= DEFAULT_COMMIT_BYTES + PLATFORM_MEMORY_PAGE_SIZE, "auto_release left % bytes committed", arena_committed_bytes(*a)); expect(a.current_point <= a.first_uncommitted_page, "current_point is past the committed region after auto_release"); p := arena_alloc(*a, 64); expect(p != null, "the arena is unusable after auto_release"); } // The overwriting variants must actually overwrite. Regression guard for H4: // a length/end-pointer mix-up made these silently do nothing. { a := make_arena(.Size_2M); defer arena_delete(*a); SIZE :: 4096; inner: *void; { auto_reset_overwriting_memory(*a); inner = arena_alloc(*a, SIZE); fill(inner, SIZE, 0xEE); expect(all_bytes_are(inner, SIZE, 0xEE), "setup failed inside the scope"); } expect(all_bytes_are(inner, SIZE, 0), "auto_reset_overwriting_memory left the region unwiped -- the wipe was skipped entirely"); expect(arena_usage_bytes(*a) == 0, "usage is % after the scope", arena_usage_bytes(*a)); } { a := make_arena(.Size_2M); defer arena_delete(*a); SIZE :: 4096; inner: *void; { auto_release_overwriting_memory(*a); inner = arena_alloc(*a, SIZE); fill(inner, SIZE, 0xEE); } // The pages stay mapped down to the initial commit, and 4096 bytes sit well inside // that, so the wiped region is still readable. expect(all_bytes_are(inner, SIZE, 0), "auto_release_overwriting_memory left the region unwiped"); expect(arena_committed_bytes(*a) == DEFAULT_COMMIT_BYTES, "committed is % after the scope", arena_committed_bytes(*a)); } // push_arena swaps context.allocator for the scope and restores it afterwards. { a := make_arena(.Size_2M); defer arena_delete(*a); outer := context.allocator; { push_arena(*a); expect(context.allocator.proc == arena_allocator_proc, "push_arena did not install the arena allocator"); expect(context.allocator.data == cast(*void) *a, "push_arena installed the wrong allocator_data"); p := New(u64); expect(p != null, "New through a pushed arena returned null"); expect(cast(*u8) p >= arena_start(*a) && cast(*u8) p < a.current_point, "New through a pushed arena did not allocate from the arena"); numbers := NewArray(64, s32); expect(numbers.count == 64, "NewArray gave % elements", numbers.count); expect(cast(*u8) numbers.data >= arena_start(*a), "NewArray did not allocate from the arena"); numbers[0] = 11; numbers[63] = 22; expect(numbers[0] == 11 && numbers[63] == 22, "arena-backed array memory is not writable"); } expect(context.allocator.proc == outer.proc, "push_arena did not restore context.allocator.proc"); expect(context.allocator.data == outer.data, "push_arena did not restore context.allocator.data"); } // A scoped mark taken inside a pushed allocator still unwinds correctly. { a := make_arena(.Size_2M); defer arena_delete(*a); point_before := a.current_point; { push_arena(*a); auto_reset(*a); s := NewArray(1024, u8); expect(s.count == 1024, "allocation inside the combined scope failed"); } expect(a.current_point == point_before, "the combined push_arena + auto_reset scope did not unwind"); } } // --------------------------------------------------------------------------- // Allocator interface: arena_allocator_proc, get_allocator, capabilities // --------------------------------------------------------------------------- allocator_tests :: () { group("allocator"); // get_allocator wires the proc to the arena. { a := make_arena(.Size_2M); defer arena_delete(*a); allocator := get_allocator(*a); expect(allocator.proc == arena_allocator_proc, "get_allocator returned the wrong proc"); expect(allocator.data == cast(*void) *a, "get_allocator returned the wrong data pointer"); } // ALLOCATE goes through arena_alloc. { a := make_arena(.Size_2M); defer arena_delete(*a); p := arena_allocator_proc(.ALLOCATE, 512, 0, null, *a); expect(p != null, "ALLOCATE returned null"); expect(cast(*u8) p >= arena_start(*a) && cast(*u8) p < a.current_point, "ALLOCATE returned a pointer outside the arena"); fill(p, 512, 0x42); expect(all_bytes_are(p, 512, 0x42), "memory from ALLOCATE is not writable"); } // RESIZE allocates fresh memory and copies the old contents across. { a := make_arena(.Size_2M); defer arena_delete(*a); OLD :: 128; NEW :: 512; old_memory := arena_allocator_proc(.ALLOCATE, OLD, 0, null, *a); fill(old_memory, OLD, 0x9C); new_memory := arena_allocator_proc(.RESIZE, NEW, OLD, old_memory, *a); expect(new_memory != null, "RESIZE returned null"); expect(new_memory != old_memory, "RESIZE returned the same block; this allocator never resizes in place"); expect(all_bytes_are(new_memory, OLD, 0x9C), "RESIZE did not copy the old contents"); // Shrinking copies only the new size, and must not read past the old block. smaller := arena_allocator_proc(.RESIZE, 64, NEW, new_memory, *a); expect(smaller != null, "a shrinking RESIZE returned null"); expect(all_bytes_are(smaller, 64, 0x9C), "a shrinking RESIZE did not copy the leading bytes"); } // FREE is accepted and does nothing. { a := make_arena(.Size_2M); defer arena_delete(*a); p := arena_allocator_proc(.ALLOCATE, 256, 0, null, *a); point_before := a.current_point; result := arena_allocator_proc(.FREE, 0, 256, p, *a); expect(result == null, "FREE returned something other than null"); expect(a.current_point == point_before, "FREE moved current_point"); } // The lifecycle and heap modes are accepted and return null. { a := make_arena(.Size_2M); defer arena_delete(*a); expect(arena_allocator_proc(.STARTUP, 0, 0, null, *a) == null, "STARTUP returned non-null"); expect(arena_allocator_proc(.SHUTDOWN, 0, 0, null, *a) == null, "SHUTDOWN returned non-null"); expect(arena_allocator_proc(.THREAD_START, 0, 0, null, *a) == null, "THREAD_START returned non-null"); expect(arena_allocator_proc(.THREAD_STOP, 0, 0, null, *a) == null, "THREAD_STOP returned non-null"); expect(arena_allocator_proc(.CREATE_HEAP, 0, 0, null, *a) == null, "CREATE_HEAP returned non-null"); expect(arena_allocator_proc(.DESTROY_HEAP, 0, 0, null, *a) == null, "DESTROY_HEAP returned non-null"); expect(is_valid(*a), "a lifecycle mode disturbed the arena"); } // IS_THIS_YOURS answers for live memory only. Regression guard for M2. { a := make_arena(.Size_2M); defer arena_delete(*a); live := arena_alloc(*a, 256); expect(arena_allocator_proc(.IS_THIS_YOURS, 0, 0, live, *a) != null, "IS_THIS_YOURS disowned a live allocation"); last_byte := cast(*u8) live + 255; expect(arena_allocator_proc(.IS_THIS_YOURS, 0, 0, last_byte, *a) != null, "IS_THIS_YOURS disowned the last live byte"); // Committed but not handed out: inside the pages, past current_point. slack := cast(*void) a.current_point; expect(arena_allocator_proc(.IS_THIS_YOURS, 0, 0, slack, *a) == null, "IS_THIS_YOURS claimed committed-but-unallocated slack"); // The header is not an allocation. expect(arena_allocator_proc(.IS_THIS_YOURS, 0, 0, cast(*void) a.memory_base, *a) == null, "IS_THIS_YOURS claimed the header region"); // Memory belonging to somebody else. stack_value := 0; expect(arena_allocator_proc(.IS_THIS_YOURS, 0, 0, *stack_value, *a) == null, "IS_THIS_YOURS claimed a stack address"); // After a reset the memory is no longer live. arena_reset(*a); expect(arena_allocator_proc(.IS_THIS_YOURS, 0, 0, live, *a) == null, "IS_THIS_YOURS still claims memory released by a reset"); } // CAPS advertises what the proc actually implements. Regression guard for M1. { a := make_arena(.Size_2M); defer arena_delete(*a); caps, name := get_capabilities(get_allocator(*a)); expect(caps & .IS_THIS_YOURS == .IS_THIS_YOURS, "CAPS does not advertise IS_THIS_YOURS, but the mode is implemented"); expect(caps & .HINT_I_AM_A_FAST_BUMP_ALLOCATOR == .HINT_I_AM_A_FAST_BUMP_ALLOCATOR, "CAPS does not advertise the bump-allocator hint"); expect(caps & .HINT_I_AM_PER_FRAME_TEMPORARY_STORAGE == 0, "CAPS claims to be per-frame temporary storage; this arena is not reset every frame"); expect(caps & .FREE == 0, "CAPS advertises FREE, but .FREE is ignored"); expect(caps & .ACTUALLY_RESIZE == 0, "CAPS advertises ACTUALLY_RESIZE, but RESIZE always reallocates"); expect(caps & .MULTIPLE_THREADS == 0, "CAPS advertises MULTIPLE_THREADS; nothing here is synchronized"); expect(caps & .CREATE_HEAP == 0, "CAPS advertises CREATE_HEAP, but the mode returns null"); expect(name.count > 0, "CAPS did not fill in the allocator name, which callers may treat as a hard error"); } // A null allocator_data must be survivable in every mode. The allocator documentation // asks for this so the proc can be pushed into a context with nothing else available, // and get_capabilities relies on it outright: it passes null for a CAPS query, because // capabilities are a property of the proc rather than of any one heap. Regression // guard for L8, which was an assert ahead of the mode switch that aborted on all of it. { expecting_errors("allocator calls with a null arena"); // CAPS is answerable with no heap at all, and must still fill in the name. name: string; caps := cast,no_check(Allocator_Caps) arena_allocator_proc(.CAPS, 0, 0, *name, null); expect(caps & .IS_THIS_YOURS == .IS_THIS_YOURS, "CAPS with a null arena lost the IS_THIS_YOURS bit"); expect(name.count > 0, "CAPS with a null arena did not fill in the allocator name"); // Nothing belongs to a heap that is not there. stack_value := 0; expect(arena_allocator_proc(.IS_THIS_YOURS, 0, 0, *stack_value, null) == null, "IS_THIS_YOURS claimed memory while holding a null arena"); // The allocating modes must decline rather than dereference. expect(arena_allocator_proc(.ALLOCATE, 64, 0, null, null) == null, "ALLOCATE with a null arena returned a pointer"); expect(arena_allocator_proc(.RESIZE, 128, 64, null, null) == null, "RESIZE with a null arena returned a pointer"); // And the modes that do nothing anyway keep doing nothing. expect(arena_allocator_proc(.FREE, 0, 0, null, null) == null, "FREE with a null arena returned non-null"); expect(arena_allocator_proc(.STARTUP, 0, 0, null, null) == null, "STARTUP with a null arena returned non-null"); expect(arena_allocator_proc(.SHUTDOWN, 0, 0, null, null) == null, "SHUTDOWN with a null arena returned non-null"); expect(arena_allocator_proc(.THREAD_START, 0, 0, null, null) == null, "THREAD_START with a null arena returned non-null"); expect(arena_allocator_proc(.THREAD_STOP, 0, 0, null, null) == null, "THREAD_STOP with a null arena returned non-null"); expect(arena_allocator_proc(.CREATE_HEAP, 0, 0, null, null) == null, "CREATE_HEAP with a null arena returned non-null"); expect(arena_allocator_proc(.DESTROY_HEAP, 0, 0, null, null) == null, "DESTROY_HEAP with a null arena returned non-null"); } // arena_alloc itself declines a null arena, which is where the guard actually lives. { expecting_errors("arena_alloc with a null arena"); expect(arena_alloc(null, 64) == null, "arena_alloc(null) returned a pointer"); expect(arena_alloc(null, 0) == null, "arena_alloc(null, 0) returned a pointer"); } // Allocation failure must surface as null through the allocator interface too, // not as a pointer into unmapped space. Regression guard for C3. { expecting_errors("allocator-interface refusal"); a := make_arena(.Size_64K); defer arena_delete(*a); p := arena_allocator_proc(.ALLOCATE, reserve_size_bytes(*a) * 4, 0, null, *a); expect(p == null, "the allocator proc returned a pointer for a request larger than the reserve"); } } // --------------------------------------------------------------------------- // Secure overwrite: arena_wipe and the arena_secure_overwrite family // --------------------------------------------------------------------------- secure_tests :: () { group("secure"); // arena_wipe clears the half-open interval it is given. { a := make_arena(.Size_2M); defer arena_delete(*a); SIZE :: 1024; p := cast(*u8) arena_alloc(*a, SIZE); fill(p, SIZE, 0xF0); arena_wipe(p, p + 256); expect(all_bytes_are(p, 256, 0), "arena_wipe did not clear the requested range"); expect(all_bytes_are(p + 256, SIZE - 256, 0xF0), "arena_wipe cleared past the end pointer"); } // An inverted or empty interval is a no-op, not a wipe of the whole address space. // This is the guard that keeps a negative count away from the rep stosb. H4. { a := make_arena(.Size_2M); defer arena_delete(*a); SIZE :: 512; p := cast(*u8) arena_alloc(*a, SIZE); fill(p, SIZE, 0x3C); arena_wipe(p + 128, p); // end < start expect(all_bytes_are(p, SIZE, 0x3C), "an inverted arena_wipe interval modified memory"); arena_wipe(p, p); // empty expect(all_bytes_are(p, SIZE, 0x3C), "an empty arena_wipe interval modified memory"); } // arena_secure_overwrite(.Current_Point) clears everything handed out so far. { a := make_arena(.Size_2M); defer arena_delete(*a); SIZE :: 2048; p := cast(*u8) arena_alloc(*a, SIZE); fill(p, SIZE, 0xA5); tail := a.current_point; // first untouched byte fill(tail, 64, 0x5A); // committed slack, must survive arena_secure_overwrite(*a, .Current_Point); expect(all_bytes_are(p, SIZE, 0), "arena_secure_overwrite(.Current_Point) left live memory unwiped"); expect(all_bytes_are(tail, 64, 0x5A), "arena_secure_overwrite(.Current_Point) wiped past current_point"); // The cursor is not moved by a wipe; only the bytes change. expect(a.current_point == tail, "arena_secure_overwrite moved current_point"); } // arena_secure_overwrite(.First_Uncommitted_Page) clears the committed slack too. { a := make_arena(.Size_2M); defer arena_delete(*a); p := cast(*u8) arena_alloc(*a, 1024); fill(p, 1024, 0xA5); slack := a.current_point; slack_count := cast(s64)(a.first_uncommitted_page - slack); expect(slack_count > 0, "no committed slack available for this test"); fill(slack, slack_count, 0x5A); arena_secure_overwrite(*a, .First_Uncommitted_Page); expect(all_bytes_are(p, 1024, 0), "the live region was not wiped"); expect(all_bytes_are(slack, slack_count, 0), "the committed slack was not wiped"); } // arena_secure_overwrite_starting_from clears from an interior point onwards. { a := make_arena(.Size_2M); defer arena_delete(*a); SIZE :: 4096; p := cast(*u8) arena_alloc(*a, SIZE); fill(p, SIZE, 0xC7); arena_secure_overwrite_starting_from(*a, p + 1024); expect(all_bytes_are(p, 1024, 0xC7), "wiping from an interior point cleared memory before it"); expect(all_bytes_are(p + 1024, SIZE - 1024, 0), "wiping from an interior point did not clear to the end"); } // A start pointer past the committed region is refused, rather than producing a // negative count. Regression guard for H4. { a := make_arena(.Size_2M); defer arena_delete(*a); p := cast(*u8) arena_alloc(*a, 256); fill(p, 256, 0x6B); arena_secure_overwrite_starting_from(*a, a.first_uncommitted_page + PLATFORM_MEMORY_PAGE_SIZE); expect(all_bytes_are(p, 256, 0x6B), "an out-of-range start pointer still wiped memory"); arena_secure_overwrite_starting_from(*a, a.first_uncommitted_page); expect(all_bytes_are(p, 256, 0x6B), "a start pointer at the committed end still wiped memory"); } // Wiping an arena with less committed than the header occupies must not underflow. // Reachable via .First_Uncommitted_Page when almost nothing is committed. H4. { a := make_arena(.Size_2M, 0); defer arena_delete(*a); // Whatever the commit works out to, this must return without wiping wildly. arena_secure_overwrite(*a, .First_Uncommitted_Page); expect(is_valid(*a), "the arena did not survive a wipe to the committed end"); p := arena_alloc(*a, 64); expect(p != null, "the arena is unusable after the wipe"); } } // --------------------------------------------------------------------------- // Platform layer: reserve, commit, decommit, lock // --------------------------------------------------------------------------- platform_tests :: () { group("platform"); // arena_platform_init reserves address space and records the class. { a: Arena; ok := arena_platform_init(*a, .Size_2M); expect(ok, "arena_platform_init reported failure for a 2M reservation"); expect(a.memory_base != null, "arena_platform_init succeeded but memory_base is null"); expect(a.reserve == .Size_2M, "arena_platform_init recorded reserve %", a.reserve); // The postcondition the rest of the module relies on: memory_base is null if and // only if the reservation failed. Regression guard for C2. expect(ok == (a.memory_base != null), "the memory_base postcondition does not hold"); // Reserved space is not yet committed; the caller drives that separately. Checked as // committed bytes rather than a null pointer because arena_platform_init now establishes // the commit pointer: memory_base for a normal reservation, and the top of the reservation // for a fully committed large-page one. expect(arena_committed_bytes(*a) == 0, "arena_platform_init committed pages on its own"); arena_delete(*a); } // Reservations at several classes, including the very large ones. { CLASSES :: Arena_Reserve.[.Size_64K, .Size_2M, .Size_64M, .Size_2G]; for class: CLASSES { a: Arena; ok := arena_platform_init(*a, class); expect(ok, "could not reserve %", class); if ok { expect(reserve_size_bytes(*a) == reserve_size_bytes(class), "reserve size mismatch for %", class); arena_delete(*a); } } } // extend_committed_pages grows the committed region by whole pages. { a := make_arena(.Size_2M); defer arena_delete(*a); before := a.first_uncommitted_page; ok := extend_committed_pages(*a, before + 1); expect(ok, "extend_committed_pages reported failure"); expect(a.first_uncommitted_page == before + PLATFORM_MEMORY_PAGE_SIZE, "a one byte extension committed % bytes", cast(s64)(a.first_uncommitted_page - before)); // The newly committed page is writable. fill(before, PLATFORM_MEMORY_PAGE_SIZE, 0x2D); expect(all_bytes_are(before, PLATFORM_MEMORY_PAGE_SIZE, 0x2D), "the newly committed page is not writable"); } // An extension to a point already committed is a no-op that reports success. // Regression guard for the delta <= 0 case. { a := make_arena(.Size_2M); defer arena_delete(*a); before := a.first_uncommitted_page; expect(extend_committed_pages(*a, before), "extending to the current boundary reported failure"); expect(a.first_uncommitted_page == before, "extending to the current boundary moved the boundary"); expect(extend_committed_pages(*a, a.memory_base), "extending backwards reported failure"); expect(a.first_uncommitted_page == before, "extending backwards moved the boundary"); expect(extend_committed_pages(*a, before - PLATFORM_MEMORY_PAGE_SIZE), "extending below the boundary reported failure"); expect(a.first_uncommitted_page == before, "extending below the boundary moved the boundary"); } // arena_commit_first_pages reports the bytes actually committed, page aligned. { a: Arena; ok := arena_platform_init(*a, .Size_2M); expect(ok, "setup reservation failed"); if ok { a.first_uncommitted_page = a.memory_base; arena_set_alignment(*a, ARENA_DEFAULT_ALIGNMENT); committed := arena_commit_first_pages(*a, 3 * PLATFORM_MEMORY_PAGE_SIZE); expect(committed == 3 * PLATFORM_MEMORY_PAGE_SIZE, "committing 3 pages reported % bytes", committed); expect(committed % PLATFORM_MEMORY_PAGE_SIZE == 0, "the reported commit % is not page aligned", committed); expect(committed == arena_committed_bytes(*a), "the report disagrees with first_uncommitted_page"); arena_delete(*a); } } // A sub-page request rounds up to a whole page. { a: Arena; if arena_platform_init(*a, .Size_2M) { a.first_uncommitted_page = a.memory_base; arena_set_alignment(*a, ARENA_DEFAULT_ALIGNMENT); committed := arena_commit_first_pages(*a, 100); expect(committed == PLATFORM_MEMORY_PAGE_SIZE, "a 100 byte commit request reported % bytes", committed); arena_delete(*a); } } // Zero and oversized requests are clamped rather than refused or overshot. { expecting_errors("zero and oversized commit requests"); a: Arena; if arena_platform_init(*a, .Size_2M) { a.first_uncommitted_page = a.memory_base; arena_set_alignment(*a, ARENA_DEFAULT_ALIGNMENT); committed := arena_commit_first_pages(*a, 0); expect(committed >= PLATFORM_MEMORY_PAGE_SIZE, "a zero commit request reported % bytes", committed); arena_delete(*a); } b: Arena; if arena_platform_init(*b, .Size_64K) { b.first_uncommitted_page = b.memory_base; arena_set_alignment(*b, ARENA_DEFAULT_ALIGNMENT); committed := arena_commit_first_pages(*b, reserve_size_bytes(*b) * 8); expect(committed <= reserve_size_bytes(*b), "an oversized commit reported % against a % reserve", committed, reserve_size_bytes(*b)); expect(committed == reserve_size_bytes(*b), "an oversized commit did not clamp to the full reserve; got %", committed); arena_delete(*b); } } // start_offset_bytes reserves extra room at the front. Currently unexercised by the // module itself, so this is the only coverage it has. { a: Arena; if arena_platform_init(*a, .Size_2M) { a.first_uncommitted_page = a.memory_base; arena_set_alignment(*a, ARENA_DEFAULT_ALIGNMENT); committed := arena_commit_first_pages(*a, PLATFORM_MEMORY_PAGE_SIZE, PLATFORM_MEMORY_PAGE_SIZE); expect(committed >= 2 * PLATFORM_MEMORY_PAGE_SIZE, "one page plus a one page offset committed only % bytes", committed); arena_delete(*a); } } // Locking and unlocking pages. These can legitimately fail on a machine with a small // working-set or RLIMIT_MEMLOCK, and the procedures log rather than assert, so the // test checks that they leave the arena intact rather than that they succeed. { a := make_arena(.Size_2M); defer arena_delete(*a); p := arena_alloc(*a, 4096); fill(p, 4096, 0x8E); arena_lock_pages(*a); expect(is_valid(*a), "the arena did not survive arena_lock_pages"); expect(all_bytes_are(p, 4096, 0x8E), "arena_lock_pages disturbed arena contents"); arena_unlock_pages(*a); expect(is_valid(*a), "the arena did not survive arena_unlock_pages"); expect(all_bytes_are(p, 4096, 0x8E), "arena_unlock_pages disturbed arena contents"); q := arena_alloc(*a, 64); expect(q != null, "the arena is unusable after a lock/unlock cycle"); } // Locking an arena with nothing committed logs rather than misbehaving. { expecting_errors("locking an arena with no active pages"); a: Arena; if arena_platform_init(*a, .Size_2M) { a.first_uncommitted_page = a.memory_base; arena_lock_pages(*a); expect(is_valid(*a), "the arena did not survive locking with no pages"); arena_delete(*a); } } // .Secure_Arena locks each range as it is committed, which is the property a one-shot lock // does not have: a lock taken at init covers only the initial commit, and everything the // arena grows into afterwards would be unlocked. Growing well past the initial commit here // is what would catch a regression back to locking once. // // Whether the locks actually succeed depends on the machine's working-set quota or // RLIMIT_MEMLOCK, so that half is reported rather than asserted -- same reasoning as the // group above. What is asserted is that the arena stays usable and its contents survive, // which is what would break if the lock/unlock bracketing around commit and decommit were // wrong. { INITIAL_PAGES :: 2; a := bootstrap_arena(.Size_2M, INITIAL_PAGES, flags = .Secure_Arena, label = "secure-growth"); expect(a != null, "bootstrap_arena with .Secure_Arena returned null"); if a { defer arena_delete(a); expect(arena_is_secure(a), "the .Secure_Arena flag did not survive bootstrap"); initial_committed := arena_committed_bytes(a); // Well past the initial commit, so the growth path runs several times. GROW_BYTES :: 32 * PLATFORM_MEMORY_PAGE_SIZE; p := arena_alloc(a, GROW_BYTES); expect(p != null, "a secure arena refused an allocation it had room for; a failed lock must not fail the commit"); if p { fill(p, GROW_BYTES, 0x5C); expect(arena_committed_bytes(a) > initial_committed, "the arena did not grow past its initial commit, so this test proves nothing"); expect(all_bytes_are(p, GROW_BYTES, 0x5C), "growing a secure arena disturbed its contents"); } // Release back down: the decommit path has to unlock first, and on Linux madvise would // otherwise refuse the locked range and leave the pages committed. arena_reset(a); free_pages_down_to(a, INITIAL_PAGES); expect(arena_committed_bytes(a) <= initial_committed, "free_pages_down_to left % bytes committed on a secure arena, expected at most %", arena_committed_bytes(a), initial_committed); q := arena_alloc(a, 64); expect(q != null, "a secure arena is unusable after a grow/release cycle"); emit(" secure arena lock intact after growth: % (quota permitting)\n", arena_secure_lock_intact(a)); } } // Large pages. Whether the OS grants them depends on privilege (SeLockMemoryPrivilege on // Windows) and, on Linux, on a hugetlbfs pool an administrator preallocated, so the grant // itself is reported rather than asserted. What is asserted holds either way: the arena // works, its page arithmetic agrees with whatever granularity it ended up with, and it // survives a grow/release cycle on both the fully-committed and the decommittable path. { emit(" platform large page size: %\n", platform_large_page_size()); INITIAL_PAGES :: 2; a := bootstrap_arena(.Size_64M, INITIAL_PAGES, flags = .Large_Pages, label = "large-pages"); expect(a != null, "bootstrap_arena with .Large_Pages returned null"); if a { defer arena_delete(a); expect(arena_is_large_pages(a), "the .Large_Pages flag did not survive bootstrap"); active := arena_large_pages_active(a); emit(" large pages active: % (page size %)\n", active, arena_page_size(a)); if active { expect(arena_page_size(a) == platform_large_page_size(), "an active large-page arena reports a % byte page size, expected %", arena_page_size(a), platform_large_page_size()); expect(arena_committed_bytes(a) % arena_page_size(a) == 0, "committed bytes % is not a whole number of % byte pages", arena_committed_bytes(a), arena_page_size(a)); } else { expect(arena_page_size(a) == PLATFORM_MEMORY_PAGE_SIZE, "an arena that fell back reports a % byte page size, expected %", arena_page_size(a), PLATFORM_MEMORY_PAGE_SIZE); } initial_committed := arena_committed_bytes(a); expect(initial_committed >= INITIAL_PAGES * arena_page_size(a), "only % bytes committed, expected at least % pages of % bytes", initial_committed, INITIAL_PAGES, arena_page_size(a)); // Sized off the arena's own page size, so this passes the initial commit whether the // pages are 4 KB or 2 MB. A fixed byte count would not grow a large-page arena at all. grow_bytes := 3 * arena_page_size(a) + 1234; p := arena_alloc(a, grow_bytes); expect(p != null, "a large-page arena refused an allocation it had room for"); if p { fill(p, grow_bytes, 0xA7); expect(all_bytes_are(p, grow_bytes, 0xA7), "growing a large-page arena disturbed its contents"); } arena_reset(a); free_pages_down_to(a, INITIAL_PAGES); fully_committed := active && LARGE_PAGES_ARE_FULLY_COMMITTED; if fully_committed { // There is no partial decommit of a large-page allocation, so this arena rewinds but // keeps its whole reservation. Asserting that pins the no-op down as deliberate. expect(arena_committed_bytes(a) == reserve_size_bytes(a), "a fully committed large-page arena reports % committed, expected the whole % byte reservation", arena_committed_bytes(a), reserve_size_bytes(a)); } else { expect(arena_committed_bytes(a) <= initial_committed, "free_pages_down_to left % bytes committed, expected at most %", arena_committed_bytes(a), initial_committed); } q := arena_alloc(a, 64); expect(q != null, "a large-page arena is unusable after a grow/release cycle"); } } // .Large_Pages together with .Secure_Arena. Large pages are non-pageable already, so the // arena locks nothing, and the pair of guarantees still has to come out intact -- including // arena_unlock_pages, which must not fail against a range that was never locked. { // One initial page, because a 2 MB reserve holds exactly one 2 MB page: asking for two // would trip the commit-size clamp and log about it. a := bootstrap_arena(.Size_2M, 1, flags = .Large_Pages | .Secure_Arena, label = "large-secure"); expect(a != null, "bootstrap_arena with .Large_Pages | .Secure_Arena returned null"); if a { defer arena_delete(a); expect(arena_is_large_pages(a) && arena_is_secure(a), "both flags did not survive bootstrap"); p := arena_alloc(a, 4096); expect(p != null, "a large-page secure arena refused an allocation it had room for"); if p { fill(p, 4096, 0x3B); expect(all_bytes_are(p, 4096, 0x3B), "a large-page secure arena disturbed its contents"); } unlock_ok := arena_unlock_pages(a); // Asserted only where it must hold: with large pages the arena locked nothing, so the // unlock has nothing to fail at. On the 4 KB fallback path a machine whose quota refused // the lock will refuse the unlock as well, which is the quota's answer, not a defect. if arena_large_pages_active(a) { expect(unlock_ok, "arena_unlock_pages failed on an arena backed by large pages, which locked nothing"); } emit(" large+secure: large pages active %, lock intact %, unlock ok % (quota permitting)\n", arena_large_pages_active(a), arena_secure_lock_intact(a), unlock_ok); } } // A reservation that cannot hold one large page has to fall back rather than refuse. The // warning reporting the fallback is the expected output, not a failure. { expecting_errors("a 64K reservation asking for large pages"); a := bootstrap_arena(.Size_64K, 2, flags = .Large_Pages, label = "large-pages-too-small"); expect(a != null, "bootstrap_arena refused instead of falling back to % byte pages", PLATFORM_MEMORY_PAGE_SIZE); if a { defer arena_delete(a); expect(!arena_large_pages_active(a), "a 64K arena claims to be backed by large pages"); expect(arena_page_size(a) == PLATFORM_MEMORY_PAGE_SIZE, "a 64K arena that fell back reports a % byte page size, expected %", arena_page_size(a), PLATFORM_MEMORY_PAGE_SIZE); p := arena_alloc(a, 64); expect(p != null, "an arena that fell back to small pages is unusable"); } } } // --------------------------------------------------------------------------- // What the numbers mean, so nobody reads more into them than is there: // // A "check" is one expect() call reached at runtime, not one distinct property. A check // inside a loop counts once per iteration, so a single stress loop can contribute more // checks than every other test in its group put together. The per-group breakdown below // makes that visible rather than burying it in one headline figure. // // Breadth is better measured by the number of expect() call sites in this file, and // better still by whether a real defect would trip one. Two did on the first run of this // suite. Neither would have been caught by adding more iterations to a loop. report :: () -> bool { emit("\n-- results by group --\n"); for 0..group_count-1 { if group_fails[it] { emit(" % -- % checks, % FAILED\n", group_names[it], group_checks[it], group_fails[it]); } else { emit(" % -- % checks\n", group_names[it], group_checks[it]); } } emit("\n"); if checks_failed == 0 { emit("ALL ARENA TESTS PASSED (% checks over % groups)\n", checks_run, group_count); return true; } emit("ARENA TESTS FAILED (% of % checks over % groups)\n", checks_failed, checks_run, group_count); return false; } #scope_export // builder null writes to stdout as it goes; pass one to buffer the whole report so a // caller running several suites at once can print each intact. See src/test/testing.jai. run_arena_tests :: (builder: *String_Builder = null) -> bool { output_builder = builder; emit("=== arena.jai / arena_platform.jai test suite ===\n"); emit("size_of(Arena) = %, page size = %, default commit = % pages\n", size_of(Arena), PLATFORM_MEMORY_PAGE_SIZE, ARENA_DEFAULT_COMMIT_PAGE_COUNT); #if RUN_LIFECYCLE_TESTS { lifecycle_tests(); } #if RUN_INTROSPECTION_TESTS { introspection_tests(); } #if RUN_ALLOCATION_TESTS { allocation_tests(); } #if RUN_BOUNDS_TESTS { bounds_tests(); } #if RUN_ALIGNMENT_TESTS { alignment_tests(); } #if RUN_RESET_TESTS { reset_tests(); } #if RUN_SCOPED_TESTS { scoped_tests(); } #if RUN_ALLOCATOR_TESTS { allocator_tests(); } #if RUN_SECURE_TESTS { secure_tests(); } #if RUN_PLATFORM_TESTS { platform_tests(); } return report(); }