kill tmp .xo-arena

This commit is contained in:
Roland Conybeare 2026-06-06 21:44:12 -04:00
commit 6fc8303602
62 changed files with 0 additions and 8604 deletions

View file

@ -1,36 +0,0 @@
# xo-arena/CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(xo_arena VERSION 1.0)
enable_language(CXX)
include(GNUInstallDirs)
include(cmake/xo-bootstrap-macros.cmake)
xo_cxx_toplevel_options3()
# ----------------------------------------------------------------
# c++ settings
# one-time project-specific c++ flags. usually empty
set(PROJECT_CXX_FLAGS "")
add_definitions(${PROJECT_CXX_FLAGS})
# ----------------------------------------------------------------
# output targets
add_subdirectory(src/arena)
add_subdirectory(utest)
# ----------------------------------------------------------------
# cmake export
xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets)
# ----------------------------------------------------------------
# docs targets depend on all the other library/utest targets
#
add_subdirectory(docs)
# end CMakeLists.txt

View file

@ -1,41 +0,0 @@
# ----------------------------------------------------------------
# for example:
# $ PREFIX=/usr/local # for example
# $ cmake -DCMAKE_MODULE_PATH=prefix -DCMAKE_INSTALL_PREFIX=$PREFIX -B .build
#
# will get
# CMAKE_MODULE_PATH
# from xo-cmake-config --cmake-module-path
#
# and expect .cmake macros in
# CMAKE_MODULE_PATH/xo_macros/xo_cxx.cmake
# ----------------------------------------------------------------
find_program(XO_CMAKE_CONFIG_EXECUTABLE NAMES xo-cmake-config REQUIRED)
if ("${XO_CMAKE_CONFIG_EXECUTABLE}" STREQUAL "XO_CMAKE_CONFIG_EXECUTABLE-NOT_FOUND")
message(FATAL "could not find xo-cmake-config executable")
endif()
message(STATUS "XO_CMAKE_CONFIG_EXECUTABLE=${XO_CMAKE_CONFIG_EXECUTABLE}")
if (XO_SUBMODULE_BUILD)
if (("${CMAKE_MODULE_PATH}" STREQUAL "") OR ("${CMAKE_MODULE_PATH}" STREQUAL prefix))
# local version of xo-cmake macros
set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/xo-cmake/cmake")
message(STATUS "CMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}")
endif()
else()
if (("${CMAKE_MODULE_PATH}" STREQUAL "") OR ("${CMAKE_MODULE_PATH}" STREQUAL prefix))
# default to typical install location for xo-project-macros
execute_process(COMMAND ${XO_CMAKE_CONFIG_EXECUTABLE} --cmake-module-path OUTPUT_VARIABLE CMAKE_MODULE_PATH)
message(STATUS "CMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}")
endif()
endif()
# needs to have been installed somewhere on CMAKE_MODULE_PATH,
# (e.g. from xo-cmake with the same value for CMAKE_INSTALL_PREFIX)
#
include(xo_macros/xo_cxx)
xo_cxx_bootstrap_message()

View file

@ -1,14 +0,0 @@
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
# note: changes to find_dependency() calls here
# must coordinate with xo_dependency() calls
# in CMakeLists.txt
#
find_dependency(xo_reflectutil)
find_dependency(indentlog)
include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Share.cmake")
check_required_components("@PROJECT_NAME@")

View file

@ -1,26 +0,0 @@
.. _AllocInfo-reference:
AllocInfo Reference
===================
Describes a single allocation.
Requires allocator configured to store per-allocation headers
Context
-------
.. ditaa::
:--scale: 0.99
+-----------------------------------------------------+
| DArena |
| DArenaIterator |
+-----------------------------------------------------+
| ArenaConfig |
+--------------+------------------------+-------------+
| | AllocInfo cBLU| |
| +------------------------+ |
| AllocError | AllocHeaderConfig | cmpresult |
| +------------------------+ |
| | AllocHeader | |
+--------------+------------------------+-------------+

View file

@ -1,55 +0,0 @@
.. _ArenaConfig-reference:
ArenaConfig Reference
=====================
Configuration for an arena allocator
Context
-------
.. ditaa::
:--scale: 0.99
+-----------------------------------------------------+
| DArena |
| DArenaIterator |
+-----------------------------------------------------+
| ArenaConfig cBLU|
+--------------+------------------------+-------------+
| | AllocInfo | |
| +------------------------+ |
| AllocError | AllocHeaderConfig | cmpresult |
| +------------------------+ |
| | AllocHeader | |
+--------------+------------------------+-------------+
.. uml::
:caption: example arena config
:scale: 99%
:align: center
object cfg<<AreanConfig>>
cfg : name = "tmp"
cfg : size = 128MB
cfg : hugepage_z = 2MB
cfg : guard_z = 8
cfg : guard_byte = 0xfd
cfg : store_header_flag = true
cfg : header_size_mask = 0xffffffff
cfg : debug_flag = false
.. code-block:: cpp
#include <xo/arena/ArenaConfig.hpp>
Class
-----
.. doxygenclass:: xo::mm::ArenaConfig
Instance Variables
------------------
.. doxygengroup:: mm-arenaconfig-instance-vars

View file

@ -1,22 +0,0 @@
# xo-arena/docs/CMakeLists.txt
xo_doxygen_collect_deps()
xo_docdir_doxygen_config()
xo_docdir_sphinx_config(
index.rst
glossary.rst
examples.rst
implementation.rst
#AAllocator-reference.rst
#IAllocator_Xfer-reference.rst
#AAllocIterator-reference.rst
ArenaConfig-reference.rst
DArena-reference.rst
AllocInfo-reference.rst
cmpresult-reference.rst
#install.rst
#introduction.rst
)
# see xo-reader/doc or xo-unit/doc for working examples
# example.rst install.rst implementation.rst

View file

@ -1,96 +0,0 @@
.. _DArena-reference:
DArena
======
Native arena allocator
Context
-------
.. ditaa::
:--scale: 0.99
+-----------------------------------------------------+
| DArena cBLU|
| DArenaIterator |
+-----------------------------------------------------+
| ArenaConfig |
+--------------+------------------------+-------------+
| | AllocInfo | |
| +------------------------+ |
| AllocError | AllocHeaderConfig | cmpresult |
| +------------------------+ |
| | AllocHeader | |
+--------------+------------------------+-------------+
.. code-block:: cpp
#include <xo/arena/DArena.hpp>
Arena memory layout
~~~~~~~~~~~~~~~~~~~
.. code-block:: text
<------------------------reserved-------------------------->
<------------committed-----------><-------uncommitted------>
<--allocated--><----available---->
XXXXXXXXXXXXXXX___________________..........................
^ ^ ^ ^
lo free limit hi
[X] allocated: in use
[_] committed: physical memory obtained
[.] uncommitted: mapped in virtual memory, not backed by memory
Representation for a single allocation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: text
free_(pre)
v
<-------------z1--------------->
< guard >< hz >< req_z >< dz >< guard >
used <== +++++++++0000zzzz@@@@@@@@@@@@@@@@@ppppppp+++++++++ ==> avail
^ ^ ^
header mem |
^ |
last_header_ free_(post)
[+] guard surrounding each allocation, for simple sanitize checks
[0] unused header bits (available for application metadata)
[z] record allocation size
[@] new allocated memory
[p] padding (to uintptr_t alignment)
Class
-----
.. doxygenclass:: xo::mm::DArena
Member Variables
----------------
.. doxygengroup:: mm-arena-instance-vars
Type Traits
-----------
.. doxygengroup:: mm-arena-traits
Constructors
------------
.. doxygengroup:: mm-arena-ctors
Methods
-------
.. doxygengroup:: mm-arena-methods

View file

@ -1,50 +0,0 @@
.. _DArenaIterator-reference:
DArenaIterator
==============
Iterator for allocs obtained from a :cpp:class:`xo::mm::DArena`.
Context
-------
.. ditaa::
:--scale: 0.99
+-----------------------------------------------------+
| DArena |
| DArenaIterator cBLU|
+-----------------------------------------------------+
| ArenaConfig |
+--------------+------------------------+-------------+
| | AllocInfo | |
| +------------------------+ |
| AllocError | AllocHeaderConfig | cmpresult |
| +------------------------+ |
| | AllocHeader | |
+--------------+------------------------+-------------+
.. code-block:: cpp
#include <xo/arena/DArenaIterator.hpp>
Class
-----
.. doxygenclass:: xo::mm::DArenaIterator
Member Variables
----------------
.. doxygengroup:: mm-arenaiterator-instance-vars
Constructors
------------
.. doxygengroup:: mm-arenaiterator-ctors
Methods
-------
.. doxygengroup:: mm-arenaiterator-methods

View file

@ -1,72 +0,0 @@
build
+-----------------------------------------------+
| cmake |
| CMakeLists.txt |
| $PREFIX/share/cmake/xo_macros/xo_cxx.cmake |
+-----------------------------------------------+
|
| +----------------------+
+------------------------------------------------->| .build/docs/Doxyfile |
| +----------------------+
| |
| /------------/
| |
| v
| +---------------------------------------+ +-----------------+
+---->| doxygen |--->| .build/docs/dox |
| | $PREFIX/share/xo-macros/Doxyfile.in | | +- html/ |
| +---------------------------------------+ | +- xml/ |
| +-----------------+
| |
| /------------/
| |
| v
| +---------------------------------------+ +--------------------+
\---->| sphinx |--->| .build/docs/sphinx |
| +- conf.py | | +- html/ |
| +- _static/ | +--------------------+
| +- *.rst |
+---------------------------------------+
files
README this file
CMakeLists.txt build entry point
conf.py sphinx config
_static static files for sphinx
map
index.rst
+- examples.rst
+- ArenaConfig-reference.rst
+- DArena-reference.rst
+- DArenaIterator-reference.rst
+- AllocInfo-reference.rst
+- cmpresult-reference.rst
+- glossary.rst
...
examples
.. doxygenclass:: ${c++ class name}
:project:
:path:
:members:
:protected-members:
:private-members:
:undoc-members:
:member-groups:
:members-only:
:outline:
:no-link:
:allow-dot-graphs:
.. doxygendefine:: ${c preprocessor define}
.. doxygenconcept:: ${c++ concept definition}
.. doxygenenum:: ${c++ enum definition}
.. doxygenfunction:: ${c++ function name}

View file

@ -1 +0,0 @@
add any static {.html, .js, ..} files for sphinx to pickup here

Binary file not shown.

Before

Width:  |  Height:  |  Size: 303 KiB

View file

@ -1,50 +0,0 @@
.. _cmpresult-reference:
cmpresult
=========
Represent the result of a partially ordered comparison
Context
-------
.. ditaa::
:--scale: 0.99
+-----------------------------------------------------+
| DArena |
| DArenaIterator |
+-----------------------------------------------------+
| ArenaConfig |
+--------------+------------------------+-------------+
| | AllocInfo | cBLU|
| +------------------------+ |
| AllocError | AllocHeaderConfig | cmpresult |
| +------------------------+ |
| | AllocHeader | |
+--------------+------------------------+-------------+
.. code-block:: cpp
#include <xo/arena/cmpresult.hpp>
Class
-----
.. doxygenclass:: xo::mm::cmpresult
Constructors
------------
.. doxygengroup:: mm-cmpresult-ctors
Methods
-------
.. doxygengroup:: mm-cmpresult-methods
Member Variables
----------------
.. doxygengroup:: mm-cmpresult-instance-vars

View file

@ -1,39 +0,0 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'xo arena documentation'
copyright = '2025, Roland Conybeare'
author = 'Roland Conybeare'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
#extensions = []
extensions = [ "breathe",
"sphinx.ext.mathjax", # inline math
"sphinx.ext.autodoc", # generate info from docstrings
"sphinxcontrib.ditaa", # diagrams-through-ascii-art
"sphinxcontrib.plantuml" # text -> uml diagrams
]
# note: breathe requires doxygen xml output -> must have GENERATE_XML = YES in Doxyfile.in
# match project name in Doxyfile.in
breathe_default_project = "xodoxxml"
templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
pygments_style = 'sphinx'
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
#html_theme = 'alabaster'
html_theme = 'sphinx_rtd_theme'
html_static_path = ['_static']
html_favicon = '_static/img/favicon.ico'

View file

@ -1,125 +0,0 @@
.. _examples:
.. toctree
:maxdepth: 2
Examples
========
Arena allocation
-----------------
.. code-block:: cpp
#include <xo/alloc2/arena/DArena.hpp>
using namespace xo::mm;
using namespace std;
Create an arena:
.. code-block:: cpp
// create arena, size 64k
DArena arena = DArena::map(ArenaConfig { .size_ = 64*1024; });
cout << arena.lo() << ".." << arena.hi();
This determines a VM memory address range.
Actually address range is rounded up to a whole number of VM pages.
Size here is a hard maximum. It cannot be changed for this arena instance.
.. code-block:: cpp
arena.reserved(); // 64k
arena.committed(); // 0k
arena.allocated(); // ok
arena.available(); // 0k
Although we know the address range for arena, it doesn't own any physical
memory yet. Two ways to commit memory:
1. Attempt allocation:
.. code-block:: cpp
std::byte * mem = arena.alloc(5*1024);
if (!mem)
throw std::runtime_error("alloc failed");
arena.reserved(); // 64k
arena.committed(); // 8k - 2 pages
arena.allocateed(); // 5k
arena.available(); // 3k
2. Expand committed memory explicitly:
.. code-block:: cpp
bool ok = arena.expand(5*1024);
assert(ok);
arena.reserved(); // 64k
arena.committed(); // 8k - 2 pages
arena.allocated(); // 0k
arena.available(); // 8k
Examining alloc metadata
------------------------
Given a successful allocation:
.. code-block:: cpp
std::size_t req_z = 5*1024;
std::byte * mem = arena.alloc(req_z);
if (!mem)
throw std::runtime_error("alloc failed");
AllocInfo info = arena.alloc_info(mem);
info.payload(); // [mem, mem + req_z (+ up to 7 bytes padding)]
info.is_valid(); // true
info.guard_lo(); // guard bytes preceding alloc
info.guard_hi(); // guard bytes following alloc
Can alternatively scan all live allocs in arena:
.. code-block:: cpp
for (AllocInfo info : arena) {
info.payload(); // allocated memory range
info.is_valid(); // true
info.guard_lo(); // guard bytes preceding alloc
info.guard_hi(); // guard bytes following alloc
}
Recycling memory
----------------
.. code-block:: cpp
// arena in non-empty state
arena.reserved(); // 64k
arena.committed(); // 8k - 2 pages
arena.allocateed(); // 5k
arena.available(); // 3k
arena.clear();
arena.reserved() // 64k
arena.committed(); // 8k - 2 pages
arena.allocated(); // 0k
arena.available(); // 8k
Memory recycled by :cpp:func:`DArena::clear()`
is available for reuse by application; it's still owned by arena.
We're just resetting the free pointer back to the beginning of arena
memory.
To release memory to the operating system, destroy arena:
.. code-block:: cpp
arena.~DArena(); // or just let arena go out of scope

View file

@ -1,24 +0,0 @@
.. _glossary:
Glossary
--------
.. glossary::
FOMO
| faceted object model
page
| a (4k) page of virtual memory.
| O/S manages virtual memory in chunks of this size.
hugepage
| large (2MB) VM page; use to reduce page fault expense and TLB pressure.
THP
| transparent huge pages
TLB
| translation lookaside buffer
VM
| virtual memory

View file

@ -1,153 +0,0 @@
.. _implementation:
Implementation
==============
Library dependency tower for *xo-arena*
.. ditaa::
+------------------------------------+
| xo_arena |
+-----------------+------------------+
| xo_indentlog | xo_reflectutil |
+-----------------+------------------+
| xo_cmake |
+------------------------------------+
Abstraction tower for *xo-arena* components (simplified)
.. ditaa::
:--scale: 0.99
+-------------------+
| DArena |
| DArenaIterator |
+-------------------+
| ArenaConfig |
+-------------------+
| auxiliary types |
+-------------------+
Abstraction tower for *xo-arena* components (detailed)
.. ditaa::
:--scale: 0.99
+-----------------------------------------------------+
| DArena |
| DArenaIterator |
+-----------------------------------------------------+
| ArenaConfig |
+--------------+------------------------+-------------+
| | AllocInfo | |
| +------------------------+ |
| AllocError | AllocHeaderConfig | cmpresult |
| +------------------------+ |
| | AllocHeader | |
+--------------+------------------------+-------------+
.. list-table:: Native Arena Allocator
:header-rows: 1
:widths: 20 90
* - Class
- Description
* - ``ArenaConfig``
- Configuration for a ``DArena`` instance
* - ``DArena``
- VM-aware arena allocator
* - ``DArenaIterator``
- Iterator over ``DArena`` allocations
.. list-table:: Auxiliary/Support Types
:header-rows: 1
:widths: 20 90
* - Class
- Description
* - ``AllocError``
- Return type for an alloc request, with error details.
* - ``AllocInfo``
- An opaque allocation. Value of an alloc-iterator.
* - ``AllocHeaderConfig``
- Per-allocator configuration of alloc headers
* - ``AllocHeader``
- Per-allocation header (8 bytes)
* - ``cmpresult``
- Result of alloc-iterator comparison
Example Object Diagram
.. uml::
:caption: representation for an arena allocator
:scale: 99%
:align: center
object darena1<<DArena>>
darena1 : config
darena1 : lo
darena1 : hi
darena1 : free
darena1 : limit
darena1 : last_error
object header1<<ArenaConfig>>
header1 : size
header1 : header
object hconfig1<<AllocHeaderConfig>>
hconfig1 : guard_z
hconfig1 : guard_byte
hconfig1 : tseq_bits
hconfig1 : age_bits
hconfig1 : size_bits
darena1 o-- header1
header1 o-- hconfig1
.. uml::
:caption: memory layout
:scale: 99%
:align: center
object darena1<<DArena>>
darena1 : config
darena1 : lo
darena1 : hi
darena1 : free
darena1 : limit
darena1 : last_error
rectangle "allocated" #90EE90 {
note as n1
lo -> free
objects here
end note
}
rectangle "available" #FFFFE0 {
note as n2
free -> limit
alloc from here
end note
}
rectangle "uncommitted" #D3D3D3 {
note as n3
limit -> hi
not mapped yet
end note
}
darena1 -[hidden]down- n1
n1 -[hidden]down- n2
n2 -[hidden]down- n3
Remarks:
* See xo-alloc2 for abstract allocator trait *AAllocator*
along with its application to *DArena*.
* We split these because in *xo-facet* we rely on *DArena* to implement
double-dispatch (two-dimensional vtables, as seen for example in CLOS, Julia, Mathematica).

View file

@ -1,31 +0,0 @@
# xo-arena documentation master file
xo-arena documentation
======================
xo-arena provides:
* Fast vm-aware arena allocation.
* Allocates uncommitted virtual memory, and commits on demand.
* When available, uses THP (Transparent Huge Pages) to mitigate pagetable pressure.
* Optional GC support, with per-alloc header.
Diagnostic features:
* with alloc headers: forward iterators over individual allocations
* configurable guard memory between allocations.
.. toctree::
:maxdepth: 2
:caption: xo-arena contents
examples
implementation
ArenaConfig-reference
DArena-reference
DArenaIterator-reference
AllocInfo-reference
cmpresult-reference
glossary
genindex
search

View file

@ -1,84 +0,0 @@
/** @file AllocError.hpp
*
* @author Roland Conybeare, Dec 2025
**/
#pragma once
#include <cstdint>
#include <cstddef>
namespace xo {
namespace mm {
enum class error : int32_t {
/** sentinel **/
invalid = -1,
/** not an error **/
ok,
/** reserved size exhauged **/
reserve_exhausted,
/** unable to commit (i.e. mprotect failure) **/
commit_failed,
/** allocation size too big (See @ref ArenaConfig::header_size_mask_) **/
header_size_mask,
/** sub_alloc not preceded by super alloc (or another sub_alloc) **/
orphan_sub_alloc,
/** attempt to call alloc_info for allocator with alloc header feature disabled
* (e.g. @ref see ArenaConfig::store_header_flag_)
**/
alloc_info_disabled,
/** attempt to call alloc_info for address not owned by allocator **/
alloc_info_address,
/** for example: alloc iteration not supported in arenas with
* AllocConfig.store_header_flag_ = false
**/
alloc_iterator_not_supported,
/** attempt to deref an iterator that does not refer to an alloc **/
alloc_iterator_deref,
/** attempt to advance an iterator that does not refer to an alloc **/
alloc_iterator_next,
};
struct AllocError {
using size_type = std::size_t;
using value_type = std::byte*;
AllocError() = default;
explicit AllocError(error err,
uint32_t seq) : error_{err},
error_seq_{seq} {}
AllocError(error err,
const char * src_fn,
uint32_t seq,
size_type req_z,
size_type com_z,
size_type rsv_z) : error_{err},
src_fn_{src_fn},
error_seq_{seq},
request_z_{req_z},
committed_z_{com_z},
reserved_z_{rsv_z} {}
static const char * error_description(error x);
/** error code **/
error error_ = error::ok;
/** source function. Typically injected with __PRETTY_FUNCTION__
* somewhere suitable on stack
**/
const char * src_fn_ = nullptr;
/** sequence# of this error.
* Each error event within an allocator gets next sequence number
**/
uint32_t error_seq_ = 0;
/** reqeust size assoc'd with errror **/
size_type request_z_ = 0;
/** committed allocator memory at time of error **/
size_type committed_z_ = 0;
/** reserved allocator memory at time of error **/
size_type reserved_z_ = 0;
};
} /*namespace mm*/
} /*namespace xo*/
/* end AllocError.hpp */

View file

@ -1,35 +0,0 @@
/** @file AllocHeader.hpp
*
* @author Roland Conybeare, Dec 2025
**/
#pragma once
#include <type_traits>
#include <cstdint>
#include <cstddef>
namespace xo {
namespace mm {
/** @brief per-alloc header
*
* Appears immediately before each allocation when
* ArenaConfig.store_header_flag_ is set.
*
* See AllocInfo.hpp for encoding of @ref repr_
**/
struct AllocHeader {
using repr_type = std::uintptr_t;
using size_type = std::size_t;
explicit AllocHeader(repr_type x) : repr_{x} {}
repr_type repr_;
};
static_assert(sizeof(AllocHeader) == sizeof(AllocHeader::repr_type));
static_assert(std::is_standard_layout_v<AllocHeader>);
}
}
/* end AllocHeader.hpp */

View file

@ -1,180 +0,0 @@
/** @file AllocHeaderConfig.hpp
*
* @author Roland Conybeare, Dec 2025
**/
#pragma once
#include "AllocHeader.hpp"
#include "padding.hpp"
#include <utility>
namespace xo {
namespace mm {
/**
* @brief specifies alloc header layout
*
* Each allocation is preceded by a 64-bit header.
* Header is split into 3 configurable-width bit fields,
* labelled (from hi to lo bit order) {tseq, age, size}.
*
* 1. tseq. seq# identifying object types; needed for gc.
* 2. gen. age cohort; increases when alloc survives gc.
* 3. size. alloc size.
*
* Arena allocator only uses size.
* X1 collector uses {tseq, gen, size}
*
* alloc header
*
* TTTTTTTTTTTTGGGGGZZZZZZZZZZZZ
* < tseq ><gen>< size >
*
* masking
*
* ..432107654321076543210 bit
*
* > < .gen_bits
* 0..............01111111 gen_mask_unshifted
* 0..011111110..........0 gen_mask_shifted
* > < gen_shift
**/
struct AllocHeaderConfig {
using repr_type = AllocHeader;
using span_type = std::pair<const std::byte *, const std::byte *>;
AllocHeaderConfig() = default;
AllocHeaderConfig(std::uint32_t gz,
std::uint8_t guard_byte,
std::uint8_t t,
std::uint8_t a,
std::uint8_t z) noexcept : guard_z_{gz},
guard_byte_{guard_byte},
tseq_bits_{t},
age_bits_{a},
size_bits_{z} {}
/** create header tuple (@p t, @p a, @p z)
* with typeseq @p t, age @p a, size @p z
**/
std::uint64_t mkheader(std::uint64_t t,
std::uint64_t a,
std::uint64_t z) const noexcept {
// don't let age wrap around.
// Expect std::min() to compile to cmov (no branch)
//
a = std::min(a, this->max_age());
uint64_t tseq_bits = (t << (age_bits_ + size_bits_)) & tseq_mask();
uint64_t age_bits = (a << size_bits_) & age_mask();
uint64_t size_bits = z & size_mask();;
return (tseq_bits | age_bits | size_bits);
}
std::uint64_t tseq_mask() const noexcept {
// e.g.
// FF FF FF 00 00 00 00 00
// with tseq_bits=24, age_bits=8, size_bits=32
//
return ((1ul << tseq_bits_) - 1) << (age_bits_ + size_bits_);
}
std::uint64_t max_age() const noexcept {
return ((1ul << age_bits_) - 1);
}
std::uint64_t age_mask() const noexcept {
// e.g.
// 00 00 00 FF 00 00 00 00
// with age_bits=8, size_bits=32
//
return this->max_age() << size_bits_;
}
std::uint64_t size_mask() const noexcept {
// e.g.
// 00 00 00 00 FF FF FF FF
// with size_bits=32
//
return ((1ul << size_bits_) - 1);
}
/** extract type id from alloc header @p hdr **/
std::uint32_t tseq(repr_type hdr) const noexcept {
// e.g.
// 0x302010
// for header
// 30 20 10 -- -- -- -- --
// with tseq_bits_ = 24, age_bits_ + size_bits_ = 40
//
return (hdr.repr_ & tseq_mask()) >> (age_bits_ + size_bits_);
}
/** extract age from alloc header @p hdr **/
std::uint32_t age(repr_type hdr) const noexcept {
// e.g.
// 0xa0
// for header
// -- -- -- a0 -- -- -- --
// with age_bits_ = 8, size_bits_ = 32
//
return (hdr.repr_ & age_mask()) >> size_bits_;
}
/** extract size from alloc header @p hdr **/
std::size_t size(repr_type hdr) const noexcept {
// e.g.
// 0x01020300
// for header
// -- -- -- -- 01 02 03 00
// with size_bits_ = 32
//
return (hdr.repr_ & size_mask());
}
/** extract padded size from alloc header @p hdr **/
std::size_t size_with_padding(repr_type hdr) const noexcept {
return padding::with_padding(this->size(hdr));
}
/** true iff sentinel tseq, flagging a forwarding pointer **/
bool is_forwarding_tseq(repr_type hdr) const noexcept {
// e.g.
// 0xFFFFFF
// i.e. header
// FF FF FF -- -- -- -- --
// with tseq_bits_ = 24, age_bits + size_bits_ = 40
//
return (hdr.repr_ & tseq_mask()) == tseq_mask();
}
bool is_size_enabled() const noexcept { return size_bits_ > 0; }
/** construct alloc header for a forwarding object **/
AllocHeader mark_forwarding_tseq(AllocHeader hdr) const noexcept {
return AllocHeader((hdr.repr_ & ~tseq_mask()) | tseq_mask());
}
/** if non-zero, allocate extra space between allocs, and fill
* with fixed test-pattern contents. Allows for simple
* runtime arena sanitizing checks.
* Will be rounded up to multiple of @ref padding::c_alloc_alignment
**/
std::uint32_t guard_z_ = 0;
/** if guard_z_ > 0, write at least that many copies
* of this guard byte following each complete allocation
**/
std::uint8_t guard_byte_ = 0xfd;
/** number of bits for tseq **/
std::uint8_t tseq_bits_ = 24;
/** number of bits for age **/
std::uint8_t age_bits_ = 8;
/** number of bits for size **/
std::uint8_t size_bits_ = 32;
};
} /*namespace mm*/
} /*namespace xo*/
/* end AllocHeaderConfig.hpp */

View file

@ -1,98 +0,0 @@
/** @file AllocInfo.hpp
*
* @author Roland Conybeare, Dec 2025
**/
#pragma once
#include "AllocHeaderConfig.hpp"
#include <utility>
namespace xo {
namespace mm {
/** @class AllocInfo
* @brief bookkeeping information for an allocation
*
* AllocInfo instances are 1:1 with sum of calls to
* {@ref AAllocator::alloc, @ref AAllocator::alloc_super}
*
**/
class AllocInfo {
public:
/** @defgroup mm-allocinfo-traits **/
///@{
using size_type = AllocHeader::size_type;
using byte = std::byte;
using span_type = std::pair<const byte *, const byte *>;
///@}
/** @defgroup mm-allocinfo-ctors **/
///@{
AllocInfo(const AllocHeaderConfig * p_cfg,
const byte * p_guard_lo,
const AllocHeader * p_hdr,
const byte * p_guard_hi) : p_config_{p_cfg},
p_guard_lo_{p_guard_lo},
p_header_{p_hdr},
p_guard_hi_{p_guard_hi} {}
/** error when alloc-header not configured **/
static AllocInfo error_not_configured(const AllocHeaderConfig * p_cfg) {
return AllocInfo(p_cfg, nullptr, nullptr, nullptr);
}
/** error on deref empty iterator **/
static AllocInfo error_invalid_iterator(const AllocHeaderConfig * p_cfg) {
return AllocInfo(p_cfg, nullptr, nullptr, nullptr);
}
///@}
/** @defgroup mm-allocinfo-methods **/
///@{
AllocHeader header() const noexcept { return *p_header_; }
/** true for non-sentinel AllocInfo instance **/
bool is_valid() const noexcept { return ((p_config_ != nullptr)
&& (p_header_ != nullptr)); }
/** true iff sentinel tseq, flagging a forwarding pointer **/
bool is_forwarding_tseq() const noexcept {
return p_config_->is_forwarding_tseq(*p_header_);
}
/** Guard bytes preceding allocation-header **/
span_type guard_lo() const noexcept;
/** Type sequence number in garbage collector **/
std::uint32_t tseq() const noexcept { return p_config_->tseq(*p_header_); }
/** Allocation age in garbage collector **/
std::uint32_t age() const noexcept { return p_config_->age (*p_header_); }
/** Allocation size (including allocator-supplied padding, excluding alloc header) **/
size_type size() const noexcept { return p_config_->size(*p_header_); }
/** Payload for this allocation. This is the memory available to application **/
span_type payload() const noexcept;
/** Guard bytes immediately following allocation **/
span_type guard_hi() const noexcept;
/** Number of guard bytes **/
size_type guard_z() const noexcept { return p_config_->guard_z_; }
/** Value (fixed test pattern) of guard byte **/
uint8_t guard_byte() const noexcept { return p_config_->guard_byte_; }
///@}
/** @defgroup mm-allocinfo-instance-vars **/
///@{
const AllocHeaderConfig * p_config_ = nullptr;
const byte * p_guard_lo_ = nullptr;
const AllocHeader * p_header_ = nullptr;
const byte * p_guard_hi_ = nullptr;
///@}
};
} /*namespace mm*/
} /*namespace xo*/
/* end AllocInfo.hpp */

View file

@ -1,71 +0,0 @@
/** @file ArenaConfig.hpp
*
* @author Roland Conybeare, Dec 2025
**/
#pragma once
#include "AllocHeaderConfig.hpp"
#include <string>
#include <cstdint>
namespace xo {
namespace mm {
/** @class ArenaConfig
*
* @brief configuration for a @ref DArena instance
**/
struct ArenaConfig {
/** @defgroup mm-arenaconfig-ctors **/
///@{
/** NOTE: not providing explicit ctors so we can use designated initializers **/
ArenaConfig with_name(std::string name) const {
ArenaConfig copy(*this);
copy.name_ = name;
return copy;
}
ArenaConfig with_size(std::size_t z) const {
ArenaConfig copy(*this);
copy.size_ = z;
return copy;
}
ArenaConfig with_store_header_flag(bool x) const {
ArenaConfig copy(*this);
copy.store_header_flag_ = x;
return copy;
}
///@}
/** @defgroup mm-arenaconfig-instance-vars ArenaConfig members **/
///@{
/** optional name, for diagnostics **/
std::string name_;
/** desired arena size -- hard max = reserved virtual memory **/
std::size_t size_ = 0;
/** hugepage size -- using huge pages relieves some TLB pressure
* (provided you use their full extent :)
**/
std::size_t hugepage_z_ = 2 * 1024 * 1024;
/** true to store header (8 bytes) at the beginning of each allocation.
* necessary and sufficient to allows iterating over allocs
* present in arena.
**/
bool store_header_flag_ = false;
/** configuration for per-alloc header **/
AllocHeaderConfig header_{};
/** true to enable debug logging **/
bool debug_flag_ = false;
///@}
};
} /*namespace mm*/
} /*namespace xo*/
/* end ArenaConfig.hpp */

View file

@ -1,58 +0,0 @@
/** @file ArenaHashMapConfig.hpp
*
* @author Roland Conybeare, Feb 2026
**/
#pragma once
#include <string>
#include <cstdint>
namespace xo {
namespace map {
/** @class ArenaHashMapConfig
*
* @brief configuration for a @ref DArenaHashMap instance
**/
struct ArenaHashMapConfig {
/** @defgroup map-arenahashmapconfig-ctors **/
///@{
ArenaHashMapConfig with_name(std::string name) const {
ArenaHashMapConfig copy(*this);
copy.name_ = name;
return copy;
}
ArenaHashMapConfig with_hint_max_capacity(std::size_t z) const {
ArenaHashMapConfig copy(*this);
copy.hint_max_capacity_ = z;
return copy;
}
ArenaHashMapConfig with_debug_flag(bool x) const {
ArenaHashMapConfig copy(*this);
copy.debug_flag_ = x;
return copy;
}
///@}
/** @defgroup mm-arenahashmapconfig-instance-vars ArenaHashMapConfig members **/
///@{
/** optional name, for diagnostics **/
std::string name_;
/** desired hard max hashmap size -> reserved virtual memory
* hint: actual max may be larger, because of power-of-2 considerations.
**/
std::size_t hint_max_capacity_ = 0;
/** true to enable debug logging **/
bool debug_flag_ = false;
///@}
};
} /*namespace map*/
} /*namespace xo*/
/* end ArenaHashMapConfig.hpp */

View file

@ -1,63 +0,0 @@
/** @file CircularBufferConfig.hpp
*
* @author Roland Conybeare, Jan 2026
**/
#pragma once
#include <string>
namespace xo {
namespace mm {
/** @class CircularBufferConfig
*
* @brief configuration for a @ref DCircularBuffer instance
**/
struct CircularBufferConfig {
/** @defgroup mm-circularbufferconfig-instance-vars CircularBufferConfig members **/
///@{
/** optional name, for diagnostics **/
std::string name_;
/** hard maximum buffer size = reserved virtual memory.
* However actual max will be this value rounded up to at least page size.
* Buffer will generally map much less than this amount of memory
**/
std::size_t max_capacity_ = 0;
/** hugepage size -- using huge pages relieves some TLB pressure,
* at expense of inefficient memory consumption for (up to two)
* partially used superpages.
**/
std::size_t hugepage_z_ = 2 * 1024 * 1024;
/** Threshold 'move efficeincy' = (move_distance / move_qty)
* applies to moving unread input to the beginning of mapped range,
* when not prevented by pinned ranges.
*
* Higher numbers reduce cpu consumption but increase memory consumption
* Reciprocal loose ceiling on relative effort that may be spent on
* moving fractional input
**/
float threshold_move_efficiency_ = 50.0;
/** lower bound for hard maximum number of capture spans.
*
* Expected use case is to track spans that are currently referenced
* (rather than copied) from outside a DCircularBuffer instance.
* Circular buffer will not unmap or overwrite memory for such spans.
*
* Expect to generally release captured spans in the same order they
* were captured. Out of order release is supported, but cost
* of out-of-order release grows
* like O(r) for r remembered spans.
*
* A typical parser will need spans to remember one line of input
**/
std::size_t max_captured_span_ = 0;
/** true to enable debug logging **/
bool debug_flag_ = false;
///@}
};
} /*namespace mm*/
} /*namespace xo*/
/* end CircularBufferConfig.hpp */

View file

@ -1,338 +0,0 @@
/** @file DArena.hpp
*
* @author Roland Conybeare, Dec 2025
**/
#pragma once
#include "ArenaConfig.hpp"
#include "AllocError.hpp"
#include "MemorySizeInfo.hpp"
#include "AllocInfo.hpp"
#include <xo/reflectutil/typeseq.hpp>
namespace xo {
namespace mm {
struct DArenaIterator; // see DArenaIterator.hpp
/** @class DArena
*
* @brief represent arena allocator state
*
* Provides minimal RAII functionality around memory mapping.
* For allocation implementation see @ref IAllocator_DArena
**/
struct DArena {
/*
* <----------------------------size-------------------------->
* <------------committed-----------><-------uncommitted------>
* <--allocated-->
*
* XXXXXXXXXXXXXXX___________________..........................
*
* [X] allocated: in use
* [_] committed: physical memory obtained
* [.] uncommitted: mapped in virtual memory, not backed by memory
*/
/** @defgroup mm-arena-traits arena type traits **/
///@{
/** @brief an amount of memory **/
using size_type = std::size_t;
/** @brief allocation pointer; use for allocation results **/
using value_type = std::byte*;
/** @brief a contiguous memory range **/
using range_type = std::pair<value_type, value_type>;
/** @brief type for allocation header (if enabled) **/
using header_type = AllocHeader;
/** integer identifying a type (see xo::facet::typeid<T>()) **/
using typeseq = xo::reflect::typeseq;
/** @brief mode argument for @ref _alloc **/
enum class alloc_mode : uint8_t {
/** ordinary alloc. Most common mode **/
standard,
/** begin a sequence of suballocs that share a single alloc header **/
super,
/** make a subsidiary allocation on behalf of a preceding super alloc.
* Will be followed by at least one more suballoc call.
**/
sub_incomplete,
/** make a subsidiary allocation that completes preceding super alloc. **/
sub_complete,
};
/** @brief Checkpoint for unwinding arena state **/
struct Checkpoint {
Checkpoint() = default;
explicit Checkpoint(std::byte * x) : free_{x} {}
std::byte * free_ = nullptr;
};
///@}
/** @defgroup mm-arena-ctors arena constructors and destructors **/
///@{
/** create arena per configuration @p cfg. **/
static DArena map(const ArenaConfig & cfg);
/** null ctor **/
DArena() = default;
/** create arena from @p cfg. Will reserve memory for allocation **/
DArena(const ArenaConfig & cfg);
/** ctor from already-mapped (but not committed) address range [lo,hi] **/
DArena(const ArenaConfig & cfg,
size_type page_z,
size_type arena_align_z,
value_type lo,
value_type hi);
/** DArena is not copyable **/
DArena(const DArena & other) = delete;
/** move ctor **/
DArena(DArena && other);
/** dtor releases mapped memory **/
~DArena();
/** move-assignment **/
DArena & operator=(DArena && other);
///@}
/** @defgroup mm-arena-methods **/
///@{
/** false -> not eligible for GC (allocates own memory + not moveable) **/
static constexpr bool is_gc_eligible() { return false; }
/** Reserved memory, in bytes. This is the maximum size of this arena. **/
size_type reserved() const noexcept { return hi_ - lo_; }
/** Allocated memory in bytes: memory consumed by allocs from this arena,
* including administrative overhead (alloc headers + guard bytes)
**/
size_type allocated() const noexcept { return free_ - lo_; }
/** Committed memory in bytes: amount of memory actually backed by physical memory **/
size_type committed() const noexcept { return committed_z_; }
/** Available committed memory.
* This is the amount of memory guaranteed to be usable for future allocs from this arena.
**/
size_type available() const noexcept { return limit_ - free_; }
/** VM page size for this arena (likely 4KB) **/
size_type page_z() const noexcept { return page_z_; }
/** Last error encountered by this arena **/
const AllocError & last_error() const noexcept { return last_error_; }
/** True iff address @p addr is owned by this arena,
* i.e. falls within [@ref lo_, @ref hi_)
**/
bool contains(const void * addr) const noexcept { return (lo_ <= addr) && (addr < hi_); }
/** True iff address @p addr is owned by this arena and in allocated regions **/
bool contains_allocated(const void * addr) const noexcept { return (lo_ <= addr) && (addr < free_); }
/** true if arena is mapped i.e. has a reserved address range **/
bool is_mapped() const noexcept { return (lo_ != nullptr) && (hi_ != nullptr); }
/** @ret iterator pointing to the first allocation in this arena **/
DArenaIterator begin() const noexcept;
/** @ret iterator pointing to just after the last allocation in this arena **/
DArenaIterator end() const noexcept;
/** @ret header for first allocation in this arena **/
AllocHeader * begin_header() const noexcept;
/** @ret location of header for next (not yet performed!)
* allocation in this arena
**/
AllocHeader * end_header() const noexcept;
/** report memory use for this arena to @p fn.
* For DArena reporting just one pool = arena's memory range
**/
void visit_pools(const MemorySizeVisitor & fn) const;
/** get header from allocated object address **/
header_type * obj2hdr(void * obj) noexcept;
/** get header from allocated object address (const version) **/
const header_type * obj2hdr(void * obj) const noexcept;
/** report alloc book-keeping info for allocation at @p mem
*
* Require:
* 1. @p mem is address returned by allocation on this arena
* i.e. by @ref IAllocator_DArena::alloc() or
* @ref IAllocator_DArena::alloc_super()
* 2. @p mem has not been invalidated since it was allocated
* i.e. by call to @ref DArena::clear
*
* Note: non-const, may stash error details
**/
AllocInfo alloc_info(value_type mem) const noexcept;
/** convenience template for allocating for a T-instance **/
template <typename T>
void * alloc_for(size_type n = sizeof(T)) {
return this->alloc(typeseq::id<T>(), n);
}
/** allocate at least @p z bytes of memory.
* Return nullptr and capture error if unable to satisfy request.
* May expand committed memory, as long as resulting committed size
* is no larger than reserved size
**/
value_type alloc(typeseq t, size_type z);
/** when store_header_flag enabled:
* like alloc(), but combine memory consumed by this alloc
* plus following consecutive sub_alloc()'s into a single header.
* otherwise equivalent to alloc()
**/
value_type super_alloc(typeseq t, size_type z);
/** when store_header_flag enabled:
* follow preceding super_alloc() by one or more sub_allocs().
* accumulate total allocated size (including padding) into
* single header. All sub_allocs() except the last must set
* @p complete_flag to false. The last sub_alloc() must set
* @p complete_flag to true.
**/
value_type sub_alloc(size_type z, bool complete_flag);
/** alloc copy of @p src **/
value_type alloc_copy(value_type src);
/** capture error information: advance error count + set last_error **/
void capture_error(error err,
const char * src_fn,
size_type target_z = 0) const;
/** alloc driver. shared by alloc(), super_alloc(), sub_alloc() **/
value_type _alloc(std::size_t req_z,
alloc_mode mode,
typeseq tseq,
uint32_t age,
const char * src_fn);
/** expand committed space in arena @p d
* to size at least @p z, on behalf of @p src_fn
* In practice will round up to a multiple of @ref page_z_.
**/
bool expand(size_type z, const char * src_fn) noexcept;
/** create initial guard **/
void establish_initial_guard() noexcept;
/** checkpoint arena state. Revert to the same state with
* @ref restore
**/
Checkpoint checkpoint() noexcept { return Checkpoint(free_); }
/** restore arena state to previously-established checkpoint **/
void restore(Checkpoint ckp) noexcept { free_ = ckp.free_; }
/** zero out all allocated memory. Likely use case is diagnostics **/
void scrub() noexcept;
/** discard all allocated memory, return to empty state
* Promise:
* - committed memory unchanged
* - available memory = committed memory
**/
void clear() noexcept;
/** release backing memory and reset bookkeeping to the empty state.
*
* Unmaps [@ref lo_, @ref hi_) (if mapped) and zeroes the bookkeeping
* fields {lo_, committed_z_, last_header_, free_, limit_, hi_,
* error_count_, last_error_}. @ref config_ (and page_z_/arena_align_z_)
* are left intact.
*
* Idempotent: a second call is a no-op (lo_ has been cleared).
* Invoked by ~DArena(); also safe to call on a live arena.
*
* Note: application code must not rely on observing the zeroed state
* after destruction (that would be UB) -- the zeroing is defensive,
* to avoid leaving dangling pointers behind.
**/
void unmap() noexcept;
/** swap contents (including configuration) with another arena **/
void swap(DArena & other) noexcept;
///@}
/** @defgroup mm-arena-instance-vars **/
///@{
/** arena configuration **/
ArenaConfig config_;
/** size of a VM page (obtained automatically via getpagesize()). Likely 4k **/
size_type page_z_ = 0;
/** alignment for this arena. In practice will be either page_z_ or cfg.hugepage_z_ **/
size_type arena_align_z_ = 0;
/** arena owns memory in range [@ref lo_, @ref hi_)
**/
std::byte * lo_ = nullptr;
/** prefix of this size is committed.
* Remainder mapped but uncommitted.
**/
size_type committed_z_ = 0;
/** if config_.store_header_flag_:
* Pointer to header for last allocation.
**/
header_type * last_header_ = nullptr;
/** free pointer.
* Memory in range [@ref lo_, @ref free_) current in use
**/
std::byte * free_ = nullptr;
/** soft limit; end of committed virtual memory
* Memory in range [@ref lo_, @ref limit_) is committed
* (backed by physical memory)
**/
std::byte * limit_ = nullptr;
/** hard limit; end of reserved virtual memory
* Memory in range [@ref limit_, @ref hi_) is uncommitted
**/
std::byte * hi_ = nullptr;
/** count runtime errors. Each error updates @ref last_error_ **/
uint32_t error_count_ = 0;
/** capture some error details if/when error **/
AllocError last_error_;
///@}
};
/** construct a @tparam T instance from arguments @p args
* using memory obtained from arena @p ialloc
**/
template <typename T,
typename... Args>
static T *
construct_with(DArena & ialloc, Args&&... args)
{
using xo::reflect::typeseq;
typeseq t = typeseq::id<T>();
std::byte * mem = ialloc.alloc(t, sizeof(T));
if (mem)
return new (mem) T(std::forward<Args>(args)...);
return nullptr;
}
} /*namespace mm*/
} /*namespace xo*/
/* end DArena.hpp */

View file

@ -1,768 +0,0 @@
/** @file DArenaHashMap.hpp
*
* @author Roland Conybeare, Jan 2026
**/
#pragma once
#include "ArenaHashMapConfig.hpp"
#include "DArenaVector.hpp"
#include "hashmap/verify_policy.hpp"
#include "hashmap/HashMapStore.hpp"
#include "hashmap/DArenaHashMapIterator.hpp"
#include <xo/indentlog/scope.hpp>
#include <algorithm>
#include <array>
#include <utility>
#include <cstring>
namespace xo {
namespace map {
#ifdef NOT_YET
enum class insert_error : int32_t {
/** sentinel **/
invalid = -1,
/** not an error **/
ok,
};
#endif
/** @brief flat hash map of key-value pairs using dedicated DArenas for storage
*
* Replicates (to the extent feasible) std::unordered_map<K,V>
*
* @tparam Key key type.
* @tparam Value value type.
* @tparam Hash hash function for keys
* @tparam Equal equality function for keys
**/
template <typename Key,
typename Value,
typename Hash = std::hash<Key>,
typename Equal = std::equal_to<void>>
struct DArenaHashMap : DArenaHashMapUtil {
public:
using size_type = DArenaHashMapUtil::size_type;
using key_type = Key;
using mapped_type = Value;
using value_type = std::pair<const Key, Value>;
using key_hash = Hash;
using key_equal = Equal;
using MemorySizeVisitor = xo::mm::MemorySizeVisitor;
using byte = std::byte;
using group_type = detail::ControlGroup;
using store_type = detail::HashMapStore<Key, Value>;
using insert_value_type = std::pair<value_type *, bool>;
using iterator = detail::DArenaHashMapIterator<Key, Value>;
using const_iterator = detail::DArenaHashMapConstIterator<Key, Value>;
public:
/** create hash map **/
DArenaHashMap(const ArenaHashMapConfig & cfg);
DArenaHashMap(const std::string & name,
size_type hint_max_capacity,
bool debug_flag = false);
DArenaHashMap(const std::string & name,
Hash && hash = Hash(),
Equal && eq = Equal(),
size_type hint_max_capacity = 0,
bool debug_flag = false);
/** true for types that support the AGCObject facet; DArenaHashMap gets its own memory! **/
static constexpr bool is_gc_eligible() { return false; }
size_type empty() const noexcept { return store_.empty(); }
size_type groups() const noexcept { return store_.n_group_; }
size_type size() const noexcept { return store_.size_; }
size_type capacity() const noexcept { return store_.capacity(); }
float load_factor() const noexcept { return store_.load_factor(); }
const_iterator cbegin() const { return this->_begin_aux(); }
const_iterator cend() const { return this->_end_aux(); }
const_iterator begin() const { return this->_begin_aux(); }
const_iterator end() const { return this->_end_aux(); }
iterator begin() { return _promote_iterator(_begin_aux()); }
iterator end() { return _promote_iterator(_end_aux()); }
void visit_pools(const MemorySizeVisitor & visitor) const {
return store_.visit_pools(visitor);
}
/** insert @p kv_pair into hash map.
* Replaces any previous value stored under the same key.
*
* Return pair retval with:
* retval.first: address of slots_[p] at which pair inserted/updated
* retval.second: true if size incremented;
*
* When table is full retval.second will be nullptr,
* with error captured in last_error_
**/
insert_value_type try_insert(const value_type & kv_pair);
/** insert @p kv_pair into hash map.
* Increase table size if necessary
**/
bool insert(const value_type & kv_pair);
/** reset to empty state **/
void clear();
/** find element with key @p key.
* @return iterator to element if found, end() otherwise
**/
const_iterator find(const key_type & key) const { return _find(key); }
iterator find(const key_type & key) { return _promote_iterator(_find(key)); }
/** establish kv pair for @p key in this table; return address of value part **/
mapped_type & operator[](const key_type & key);
/** verify DArenaHashMap invariants
* Act on failure according to policy @p
* (combination of throw|log bits)
**/
bool verify_ok(verify_policy p = verify_policy::throw_only()) const;
store_type * _store() noexcept { return &store_; }
auto _hash(const key_type & key) const {
size_type h = hash_(key);
size_type h1 = h >> 7; // slot#
size_type h2 = h % 0x7f; // fingerprint
size_type N = store_.capacity();
size_type slot_ix = h1 % (N - 1);
return std::make_pair(slot_ix, h2);
}
private:
iterator _promote_iterator(const_iterator ix) {
return iterator(const_cast<uint8_t *>(ix._ctrl()),
const_cast<value_type *>(ix._pos()));
}
const_iterator _begin_aux() const {
if (this->empty()) [[unlikely]] {
return this->end();
}
const_iterator ix(&(store_.control_[c_control_stub]),
&(store_.slots_[0]));
if (ix._at_slot_sentinel()) {
/* advance to first occupied position in table */
++ix;
}
return ix;
}
const_iterator _end_aux() const {
const_iterator ix(&(store_.control_[c_control_stub + store_.capacity()]),
&(store_.slots_[store_.capacity()]));
return ix;
}
/** search hash map on key @p key, return iterator to table member.
* return end-iterator if @p key not found
**/
const_iterator _find(const key_type & key) const;
/** insert @p kv_pair,
* where key hashes to @p hash_value, into @p *store
**/
insert_value_type _try_insert_aux(size_type hash_value,
const value_type & kv_pair,
store_type * p_store);
/** increase hash table size (invoke when max load factor reached) **/
bool _try_grow();
/** load group abstraction from control bytes starting at @p ix **/
group_type _load_group(size_type ix) { return store_._load_group(ix); }
/** like ctrl_[ix] = h2, but maintain overflow copy
* at end of ctrl_[] array
**/
void _update_control(size_type ix, uint8_t h2) {
return store_._update_control(ix, h2);
}
private:
/** hash function **/
key_hash hash_;
/** key equal **/
key_equal equal_;
/** hash table state contents + size-related attributes **/
store_type store_;
/** true to enable debug logging **/
bool debug_flag_ = false;
};
template <typename Key, typename Value, typename Hash, typename Equal>
DArenaHashMap<Key, Value, Hash, Equal>::DArenaHashMap(const ArenaHashMapConfig & cfg)
: DArenaHashMap(cfg.name_, Hash(), Equal(), cfg.hint_max_capacity_, cfg.debug_flag_)
{
}
template <typename Key, typename Value, typename Hash, typename Equal>
DArenaHashMap<Key, Value, Hash, Equal>::DArenaHashMap(const std::string & name,
size_type hint_max_capacity,
bool debug_flag)
: DArenaHashMap(name, Hash(), Equal(), hint_max_capacity, debug_flag)
{
}
/* remarks:
* - control: extra 16 slots for safe wraparound.
* last 16 bytes will be copy of first 16 bytes
*/
template <typename Key, typename Value, typename Hash, typename Equal>
DArenaHashMap<Key, Value, Hash, Equal>::DArenaHashMap(const std::string & name,
Hash && hash,
Equal && eq,
size_type hint_max_capacity,
bool debug_flag)
: hash_{std::move(hash)},
equal_{std::move(eq)},
store_{name, lub_exp2(lub_group_mult(hint_max_capacity))},
debug_flag_{debug_flag}
{
}
template <typename Key, typename Value, typename Hash, typename Equal>
auto
DArenaHashMap<Key,
Value,
Hash,
Equal>::try_insert(const value_type & kv_pair) -> insert_value_type
{
size_type h = hash_(kv_pair.first);
return _try_insert_aux(h, kv_pair, &store_);
}
template <typename Key,
typename Value,
typename Hash,
typename Equal>
auto
DArenaHashMap<Key,
Value,
Hash,
Equal>::_try_insert_aux(size_type hash_value,
const std::pair<const Key, Value> & kv_pair,
store_type * p_store)
-> std::pair<value_type *, bool>
{
scope log(XO_DEBUG(false));
size_type h = hash_value;
// h1: hi bits: probe sequence
size_type h1 = h >> 7;
// h2: lo bits: store in control byte
uint8_t h2 = h & 0x7f;
size_type N = p_store->capacity();
if (N == 0) [[unlikely]] {
return std::make_pair(nullptr, false);
}
// same as:
// ix = h1 % N
// since N is power of 2
size_type ix = h1 & (N - 1);
// will make series of probes
for (;;) {
auto grp = p_store->_load_group(ix);
{
// look for matching slot to update
uint16_t m = grp.all_matches(h2);
// process each match.
// matches are encountered in the same order they
// appear in ctrl_[]
while (m) {
// zeroes: #of 0 before least-significant 1 bit
int skip = __builtin_ctz(m);
size_type slot_ix = (ix + skip) & (N - 1);
// invariant: slot_ix in [0 .. N)
auto & slot = p_store->slots_[slot_ix];
if (equal_(slot.first, kv_pair.first)) {
// we have match on existing key;
// replace associated value
slot.second = kv_pair.second;
// false: did not change table size
return std::make_pair(&slot, false);
}
// e.g:
// /-- lowest 1 bit gets cleared
// v
// m = b01101000
// m-1 = b01100111
// & = b01100000
m &= (m - 1);
}
}
{
// look for empty slot to insert
uint16_t e = grp.empty_matches();
// process each empty slot
if (e) {
// check that table is below max load factor (0.875).
// Check here so that table can stay at max load factor
// indefinitely as long as updates only
//
if (p_store->load_factor() >= c_max_load_factor) {
return std::make_pair(nullptr, false);
}
// zeroes: #of 0 before least significant 1 bit
int skip = __builtin_ctz(e);
size_type slot_ix = (ix + skip) & (N - 1);
// invariant: slot_ix in [0 .. N)
auto & slot = p_store->slots_[slot_ix];
// mark slot occupied in control space;
// maintain copy-at-end for overflow
p_store->_update_control(slot_ix, h2);
new (&slot) value_type(kv_pair);
++(p_store->size_);
// true: increased table size
return std::make_pair(&slot, true);
}
}
// slot range associated with grp
// has no room, and does not contain target key
// -> move on to next group.
//
// note: relying on c_group_size overflow bytes here
// when ix is close to N
ix = (ix + c_group_size) & (N - 1);
}
}
template <typename Key, typename Value, typename Hash, typename Equal>
bool
DArenaHashMap<Key, Value, Hash, Equal>::_try_grow()
{
scope log(XO_DEBUG(false));
size_type n_group_exponent_2x = 0;
size_type n_group_2x = 0;
if (store_.n_group_ == 0) [[unlikely]] {
// special case: grow from hard empty state
n_group_exponent_2x = 0;
n_group_2x = 1;
} else {
n_group_exponent_2x = store_.n_group_exponent_ + 1;
n_group_2x = 2 * n_group_exponent_2x;
}
// optimization when table is empty. in that case can resize
// arenas in place
if (this->empty()) {
log && log("resize-from-empty branch");
this->store_.resize_from_empty(std::make_pair(n_group_exponent_2x, n_group_2x));
} else {
log && log("duplicate-and-replace branch");
detail::HashMapStore<Key, Value> store_2x("arenahashmap",
std::make_pair(n_group_exponent_2x,
n_group_2x));
/* rehash everything in store_,
* into store_2x
*/
for (size_type i = 0, n = store_.capacity(); i < n; ++i) {
uint8_t ctrl = store_.control_[c_control_stub + i];
value_type & kv_pair = store_.slots_[i];
if (DArenaHashMapUtil::is_data(ctrl)) {
size_type h = hash_(kv_pair.first);
auto chk = this->_try_insert_aux(h, kv_pair, &store_2x);
if (!chk.second) {
// shenanigans - something isn't right.
// - may have run out of memory
assert(false);
return false;
}
}
}
this->store_ = std::move(store_2x);
}
return true;
}
template <typename Key,
typename Value,
typename Hash,
typename Equal>
bool
DArenaHashMap<Key,
Value,
Hash,
Equal>::insert(const std::pair<const Key, Value> & kv_pair)
{
scope log(XO_DEBUG(false));
auto [slot_addr, ins_flag] = this->try_insert(kv_pair);
if (slot_addr) {
log && log("fast", xtag("slot_addr", (void*)slot_addr), xtag("ins_flag", ins_flag));
return ins_flag;
}
assert((store_.size_ + 1) / static_cast<float>(store_.n_slot_) >= c_max_load_factor);
if (this->_try_grow()) {
/* retry insert, with bigger table */
auto [slot_addr, ins_flag] = this->try_insert(kv_pair);
return ins_flag;
} else {
assert(false);
// TODO: set last error. Presumeably reached max size
return false;
}
}
template <typename Key,
typename Value,
typename Hash,
typename Equal>
void
DArenaHashMap<Key, Value, Hash, Equal>::clear()
{
this->store_.clear();
}
template <typename Key,
typename Value,
typename Hash,
typename Equal>
auto
DArenaHashMap<Key, Value, Hash, Equal>::_find(const key_type & key) const -> const_iterator
{
size_type N = store_.capacity();
if (N == 0) [[unlikely]] {
return this->cend();
}
size_type h = hash_(key);
size_type h1 = h >> 7;
uint8_t h2 = h & 0x7f;
size_type ix = h1 & (N - 1);
for (;;) {
auto grp = store_._load_group(ix);
{
uint16_t m = grp.all_matches(h2);
while (m) {
int skip = __builtin_ctz(m);
size_type slot_ix = (ix + skip) & (N - 1);
auto & slot = store_.slots_[slot_ix];
if (equal_(slot.first, key)) {
return const_iterator(&(store_.control_[c_control_stub + slot_ix]),
&slot);
}
m &= (m - 1);
}
}
{
uint16_t e = grp.empty_matches();
if (e) {
return this->end();
}
}
ix = (ix + c_group_size) & (N - 1);
}
}
template <typename Key,
typename Value,
typename Hash,
typename Equal>
auto
DArenaHashMap<Key, Value, Hash, Equal>::operator[](const key_type & key) -> mapped_type &
{
{
auto ix = this->find(key);
if (ix != this->end())
return ix->second;
}
// key-value pair
value_type kv_pair = std::make_pair(key, mapped_type{});
auto [slot_addr, ins_flag] = this->try_insert(kv_pair);
if (slot_addr)
return slot_addr->second;
if (!this->_try_grow()) {
// we are out of room
throw std::runtime_error("DArenaHashMap::operator[]: table capacity exhausted");
}
/* retry insert, now with bigger capacity */
std::tie(slot_addr, ins_flag) = this->try_insert(kv_pair);
assert(slot_addr);
return slot_addr->second;
}
/**
* Verify DArenaHashMap class invariants.
*
* SM1. size consistency
* - SM1.1 size_ <= n_slot_
* - SM1.2 control_[] size consistent with slots_[] size
* - SM1.3 n_group_ consistent with n_group_exponent_
* - SM1.4 n_slot_ consistent with n_group_
* - SM1.5 n_slot_ a power of 2
* SM2. load factor
* - SM2.1 load_factor() <= c_max_load_factor
* SM3. control_
* - SM3.1 control_[i] = c_iterator_bookend for i in [0, c_control_stub)
* - SM3.2 control_[stub+i] = control_[stub+N+i] for i in [0, c_group_size)
* - SM3.3 {number of control_[i] spots with non-sentinel values} = size_
* - SM3.4 control_[stub+N+c_group_size+i] = c_iterator_bookend for i in [0, c_control_stub)
* SM4. slots_
* - SM4.1 if control_[i] is non-sentinel:
* - SM4.1.1 control_[i] = hash_(slots_[i].first) & 0x7f
* - SM4.1.2 all slots in range [h .. i] are non-empty,
* where h is hash_(slots_[i].first >> 7
* - SM4.2 if control_[i] is empty or tombstone:
* - slots_[i].first = key_type()
*
**/
template <typename Key, typename Value, typename Hash, typename Equal>
bool
DArenaHashMap<Key, Value, Hash, Equal>::verify_ok(verify_policy policy) const
{
using xo::scope;
using xo::tostr;
using xo::xtag;
constexpr const char * c_self = "DArenaHashMap::verify_ok";
scope log(XO_DEBUG(debug_flag_),
xtag("size", store_.size_));
/* SM1.1: size_ <= n_slot_ */
if (store_.size_ > store_.n_slot_) {
return policy.report_error(log,
c_self, ": expect .size <= .n_slot",
xtag("size", store_.size_),
xtag("n_slot", store_.n_slot_));
}
/* SM1.2: control_[] size consistent with slots_[] size */
if (store_.control_.size() != control_size(store_.n_slot_)) {
return policy.report_error
(log,
c_self, ": expect .control_.size = .n_slot + c_group_size + 2 * c_control_stub",
xtag("control_.size", store_.control_.size()),
xtag("n_slot", store_.n_slot_),
xtag("c_group_size", c_group_size),
xtag("c_control_stub", c_control_stub));
}
if (store_.slots_.size() != store_.n_slot_) {
return policy.report_error(log,
c_self, ": expect .slots_.size = .n_slot",
xtag("slots_.size", store_.slots_.size()),
xtag("n_slot", store_.n_slot_));
}
/* SM1.3: n_group_ consistent with n_group_exponent_ */
if ((store_.n_group_ > 0)
&& (store_.n_group_ != (size_type{1} << store_.n_group_exponent_)))
{
return policy.report_error(log,
c_self, ": expect .n_group = 2^.n_group_exponent",
xtag("n_group", store_.n_group_),
xtag("n_group_exponent", store_.n_group_exponent_));
}
/* SM1.4: n_slot_ consistent with n_group_ */
if (store_.n_slot_ != store_.n_group_ * c_group_size) {
return policy.report_error(log,
c_self, ": expect .n_slot = .n_group * c_group_size",
xtag("n_slot", store_.n_slot_),
xtag("n_group", store_.n_group_),
xtag("c_group_size", c_group_size));
}
/* SM1.5: n_slot_ a power of 2 */
if ((store_.n_slot_ & (store_.n_slot_ - 1)) != 0) {
return policy.report_error(log,
c_self, ": expect .n_slot is power of 2",
xtag("n_slot", store_.n_slot_));
}
/* SM2.1: load_factor() <= c_max_load_factor */
if (load_factor() > c_max_load_factor) {
return policy.report_error(log,
c_self, ": expect .load_factor <= c_max_load_factor",
xtag("load_factor", load_factor()),
xtag("c_max_load_factor", c_max_load_factor));
}
/* SM3.1: control_[i] = c_iterator_bookend for i in [0, c_control_stub) */
for (size_type i = 0; i < c_control_stub; ++i) {
if (store_.control_[i] != c_iterator_bookend) {
return policy.report_error(log,
c_self, ": expect control_[i] = c_iterator_bookend for front stub",
xtag("i", i),
xtag("control_[i]", (int)(store_.control_[i])),
xtag("c_iterator_bookend", (int)c_iterator_bookend));
}
}
/* SM3.2: control_[N+i] = control_[i] for i in [0, c_group_size) */
for (size_type i = 0; i < c_group_size; ++i) {
if (store_.control_[store_.n_slot_ + i + c_control_stub] != store_.control_[i + c_control_stub]) {
return policy.report_error(log,
c_self, ": expect control_[N+i] = control_[i]",
xtag("i", i),
xtag("control_[i]", (int)(store_.control_[i + c_control_stub])),
xtag("control_[N+i]", (int)(store_.control_[store_.n_slot_ + i + c_control_stub])));
}
}
/* SM3.3: {number of control_[i] spots with non-sentinel values} = size_ */
{
size_type occupied_count = 0;
for (size_type i = 0; i < store_.n_slot_; ++i) {
uint8_t c = store_.control_[i + c_control_stub];
if (DArenaHashMapUtil::is_data(c)) {
++occupied_count;
}
}
if (occupied_count != store_.size_) {
return policy.report_error(log,
c_self, ": expect occupied control count = size",
xtag("occupied_count", occupied_count),
xtag("size", store_.size_));
}
}
/* SM3.4: control_[stub+N+c_group_size+i] = c_iterator_bookend for i in [0, c_control_stub) */
if (store_.n_slot_ > 0) {
for (size_type i = 0; i < c_control_stub; ++i) {
size_type ix = c_control_stub + store_.n_slot_ + c_group_size + i;
if (store_.control_[ix] != c_iterator_bookend) {
return policy.report_error
(log,
c_self, ": expect control_[stub+N+group+i] = c_iterator_bookend for end stub",
xtag("i", i),
xtag("N", store_.n_slot_),
xtag("ix", ix),
xtag("control_[ix]", (int)(store_.control_[ix])),
xtag("c_iterator_bookend", (int)c_iterator_bookend));
}
}
}
/* SM4.1.1: if control_[i] is non-sentinel, control_[i] = hash_(slots_[i].first) & 0x7f */
for (size_type i = 0; i < store_.n_slot_; ++i) {
uint8_t c = store_.control_[i + c_control_stub];
if (DArenaHashMapUtil::is_data(c)) {
uint8_t expected_h2 = hash_(store_.slots_[i].first) & 0x7f;
if (c != expected_h2) {
return policy.report_error(log,
c_self, ": expect control[i] = hash(key) & 0x7f",
xtag("i", i),
xtag("control[i+stub]", c),
xtag("expected_h2", expected_h2));
}
}
}
/* SM4.1.2: if control_[i] is non-sentinel, all slots in range [h .. i] are non-empty,
* where h = (hash_(slots_[i].first) >> 7) & (n_slot_ - 1)
*/
for (size_type i = 0; i < store_.n_slot_; ++i) {
uint8_t c = store_.control_[i + c_control_stub];
if (DArenaHashMapUtil::is_data(c)) {
size_type h = (hash_(store_.slots_[i].first) >> 7) & (store_.n_slot_ - 1);
size_type j = h;
while (j != i) {
uint8_t cj = store_.control_[j + c_control_stub];
if (DArenaHashMapUtil::is_sentinel(cj)) {
return policy.report_error(log,
c_self, ": expect non-empty slot in probe range [h..i]",
xtag("i", i),
xtag("h", h),
xtag("j", j),
xtag("control[j+stub]", cj));
}
j = (j + 1) & (store_.n_slot_ - 1);
}
}
}
/* SM4.2: if control_[i] is empty or tombstone, slots_[i].first = key_type() */
for (size_type i = 0; i < store_.n_slot_; ++i) {
uint8_t c = store_.control_[i + c_control_stub];
if (DArenaHashMapUtil::is_sentinel(c)) {
if (!(store_.slots_[i].first == key_type())) {
return policy.report_error(log,
c_self, ": expect empty/tombstone slot has default key",
xtag("i", i),
xtag("control[i+stub]", c));
}
}
}
return true;
}
} /*namespace map*/
} /*namespace xo*/
/* end DArenaHashMap.hpp */

View file

@ -1,127 +0,0 @@
/** @file DArenaIterator.hpp
*
* @author Roland Conybeare, Dec 2025
**/
#pragma once
#include "AllocInfo.hpp"
#include "AllocHeader.hpp"
#include "cmpresult.hpp"
namespace xo {
namespace mm {
struct DArena;
/** @class DArenaIterator
* @brief Representation for alloc iterator over arena
*
* Map showing an arena allocation:
*
* @verbatim
*
* <-------------z1--------------->
* < guard >< hz >< req_z >< dz >< guard >
*
* +++++++++0000zzzz@@@@@@@@@@@@@@@@@ppppppp+++++++++
*
* ^ ^ ^
* header mem header
* ^ (next alloc)
* DArenaIterator::pos_
*
* guard [+] guard before+after each allocation, for simple sanitize checks
* header [0] alloc header (non-size bits)
* [z] alloc header (size bits)
* mem [@] app-requested memory, including padding [p]
* dz [p] padding (to uintptr_t alignment. req_z+dz recorded in header)
* free_ DArena::free_ just after guard bytes for last allocation
*
* @endverbatim
**/
struct DArenaIterator {
/** @defgroup mm-arenaiterator-ctors DArenaIterator instance vars **/
///@{
DArenaIterator() = default;
DArenaIterator(const DArena * arena,
AllocHeader * pos) : arena_{arena},
pos_{pos} {}
/** Create iterator in invalid state **/
static DArenaIterator invalid() { return DArenaIterator(); }
/** Create iterator pointing to the beginning of @p arena
* Iterator cannot modify memory, but can capture
* an iterator error in @p *arena
**/
static DArenaIterator begin(const DArena * arena);
/** Create iterator pointing to the end of @p arena
* Iterator cannot modify memory, but can capture
* an iterator error in @p *arena
**/
static DArenaIterator end(const DArena * arena);
///@}
/** @defgroup mm-arenaiterator-methods DArenaIterator methods **/
///@{
/** Address of allocation header for beginning of alloc range in @p arena **/
static AllocHeader * begin_header(const DArena * arena);
/** Address of allocation header for end of alloc range.
* This is the address of header for _next_ allocation in @p arena
* i.e. free pointer
**/
static AllocHeader * end_header(const DArena * arena);
/** A valid iterator can be compared, at least with itself
* It can be dereferenced if is also non-empty
**/
bool is_valid() const noexcept { return (arena_ != nullptr) && (pos_ != nullptr); }
/** An invalid (or sentinel) iterator is incomparable with all
* iterators including itself
**/
bool is_invalid() const noexcept { return !is_valid(); }
/** fetch contents at current iterator position **/
AllocInfo deref() const noexcept;
/** compare two iterators. To be comparable,
* iterators must refer to the same arena
**/
cmpresult compare(const DArenaIterator & other) const noexcept;
/** advance iterator to next allocation **/
void next() noexcept;
/** cast iterator position to byte* */
std::byte * pos_as_byte() const { return (std::byte *)pos_; }
/** *ix synonym for ix.deref() **/
AllocInfo operator*() const noexcept { return this->deref(); }
/** ++ix synonym for ix.next() **/
DArenaIterator & operator++() noexcept { this->next(); return *this; }
///@}
/** @defgroup mm-arenaiterator-instance-vars **/
///@{
/** iterator visits allocations from this arena **/
const DArena * arena_ = nullptr;
/** current iterator position **/
AllocHeader * pos_ = nullptr;
///@}
};
inline bool
operator==(const DArenaIterator & x,
const DArenaIterator & y)
{
return x.compare(y).is_equal();
}
inline bool
operator!=(const DArenaIterator & x,
const DArenaIterator & y)
{
return !x.compare(y).is_equal();
}
} /*namespace mm*/
} /*namespace xo*/
/* end DArenaIterator.hpp */

View file

@ -1,358 +0,0 @@
/** @file DArenaVector.hpp
*
* @author Roland Conybeare, Jan 2026
**/
#pragma once
#include "DArena.hpp"
#include <stdexcept>
#include <cstring> // for ::memset()
namespace xo {
namespace mm {
/** @brief vector of T using dedicated DArena for storage
*
* Replicate (to the extent feasible) std::vector<T>
* behavior, but using a dedicated DArena to provide storage
*
* Unlike std::vector:
* 1. does not support copying
* 2. capacity fixed at construction time
*
* @tparam T element type. Must be Erasable
**/
template <typename T>
struct DArenaVector {
public:
using value_type = T;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
using reference = value_type &;
using const_reference = const value_type &;
using iterator = value_type *;
using const_iterator = const value_type *;
/** null ctor **/
DArenaVector() = default;
/** ctor from already-mapped (but not committed) address range_type
* vector has size zero
**/
DArenaVector(const ArenaConfig & cfg,
size_type page_z,
size_type arena_align_z,
DArena::value_type lo,
DArena::value_type hi);
/** not intended to be copyable **/
DArenaVector(const DArenaVector &) = delete;
/** move ctor **/
DArenaVector(DArenaVector && other);
/** releases mapped memory **/
~DArenaVector();
/** create empty vector using @p cfg to configure backing store **/
static DArenaVector map(const ArenaConfig & cfg);
/** true iff vector is emtpy **/
bool empty() const { return size_ == 0; }
size_type size() const { return size_; }
size_type max_size() const { return capacity(); }
size_type capacity() const { return store_.reserved() / sizeof(T); }
/** get reference to element at zero-based index @p i. Do not check bounds **/
T & operator[](size_t i) noexcept { return *(this->_address_of(i)); }
const T & operator[](size_t i) const noexcept { return *(this->_address_of(i)); }
/** get reference to element at zero-based index @p i. Do check bounds **/
T & at(size_type i) { _check_valid_index(i); return *(this->_address_of(i)); }
const T & at(size_type i) const { _check_valid_index(i); return *(this->_address_of(i)); }
/** get to at first element of vector. Same as @p end if vector is empty **/
iterator begin() noexcept { return this->_address_of(0); }
/** get iterator to end of vector - "one past the last element" **/
iterator end() noexcept { return this->_address_of(size_); }
const_iterator cbegin() const noexcept { return this->_address_of(0); }
const_iterator begin() const noexcept { return this->cbegin(); }
const_iterator cend() const noexcept { return this->_address_of(size_); }
const_iterator end() const noexcept { return this->cend(); }
constexpr const DArena * store() const { return &store_; }
constexpr T * data() { return reinterpret_cast<T*>(store_.lo_); }
constexpr const T * data() const { return reinterpret_cast<const T*>(store_.lo_); }
/** arena used for element storage
* (Might prefer obj<AResourceVisitor> here; refrain to avoid leveling violation)
**/
void visit_pools(const MemorySizeVisitor & fn) const { store_.visit_pools(fn); }
/** reserve space, if possible, for at least @p z elements.
* Always limited by ArenaConfig.size_
**/
void reserve(size_type z);
/** resize to size @p z. Return true on success. May fail iff oom. **/
bool resize(size_type z);
void shrink_to_fit();
/** reset vector to empty state **/
void clear();
T & insert(size_type pos, T && x);
T & insert(size_type pos, const T & x);
void erase(size_type pos);
void push_back(T && x);
void push_back(const T & x);
void swap(DArenaVector & other) noexcept;
DArenaVector & operator=(DArenaVector && x) noexcept;
private:
T * _address_of(size_type i) { return ((T *)store_.lo_) + i; }
const T * _address_of(size_type i) const { return ((const T *)store_.lo_) + i; }
void _check_valid_index(size_type i) const;
private:
size_type size_ = 0;
DArena store_;
DArena::Checkpoint zero_ckp_;
};
template <typename T>
DArenaVector<T>::DArenaVector(const ArenaConfig & cfg,
size_type page_z,
size_type arena_align_z,
DArena::value_type lo,
DArena::value_type hi)
: store_{cfg, page_z, arena_align_z, lo, hi},
zero_ckp_{store_.checkpoint()}
{}
template <typename T>
DArenaVector<T>::DArenaVector(DArenaVector && other)
: size_{other.size_}, store_{std::move(other.store_)}, zero_ckp_{std::move(other.zero_ckp_)}
{
other.size_ = 0;
other.zero_ckp_ = DArena::Checkpoint();
}
template <typename T>
DArenaVector<T>::~DArenaVector()
{
if constexpr (std::is_trivially_destructible_v<T>) {
// nothing to do
} else {
// invoke destructor for each element
for (size_type i = 0, n = size(); i < n; ++i) {
T & x = (*this)[i];
x.~T();
}
}
}
template <typename T>
DArenaVector<T> &
DArenaVector<T>::operator=(DArenaVector && other) noexcept
{
this->size_ = other.size_;
this->store_ = std::move(other.store_);
this->zero_ckp_ = std::move(other.zero_ckp_);
other.size_ = 0;
other.zero_ckp_ = DArena::Checkpoint();
return *this;
}
template <typename T>
DArenaVector<T>
DArenaVector<T>::map(const ArenaConfig & cfg)
{
DArenaVector<T> retval;
retval.store_ = std::move(DArena::map(cfg));
retval.zero_ckp_ = retval.store_.checkpoint();
return retval;
}
template <typename T>
void
DArenaVector<T>::reserve(size_type z) {
store_.expand(z * sizeof(T), __PRETTY_FUNCTION__);
}
template <typename T>
bool
DArenaVector<T>::resize(size_type z) {
// new arena size in bytes
size_t req_z = z * sizeof(T);
if (z > size_) {
// expand arena to accomodate
if (!store_.expand(req_z, __PRETTY_FUNCTION__))
return false;
// run ctors
if constexpr (std::is_trivially_constructible_v<T>) {
::memset(this->_address_of(size_), 0, req_z - (size_ * sizeof(T)));
} else {
for (size_type i = size_; i < z; ++i) {
void * addr = &(*this)[i];
new (addr) T();
}
}
} else {
if constexpr (std::is_trivially_destructible_v<T>) {
// nothing to do
} else {
// invoke destructor for each discarded element
for (size_type i = z; i < size_; ++i) {
T & x = (*this)[i];
x.~T();
}
}
}
// rewind to checkpoint, then reallocate.
// This is for form's sake, so that DArena considers memory
// to be 'allocated'. DArenaVector<T> doesn't care for itself,
// but this preserves expected behavior of visit_pools().
//
store_.restore(zero_ckp_);
store_.alloc(xo::reflect::typeseq::id<std::byte>(), req_z);
this->size_ = z;
return true;
}
template <typename T>
void
DArenaVector<T>::shrink_to_fit() {
// could in principle release unused mapped pages here
}
template <typename T>
void
DArenaVector<T>::clear() {
this->resize(0);
}
template <typename T>
void
DArenaVector<T>::_check_valid_index(size_type i) const {
if (size_ <= i)
throw std::out_of_range("DArenaVector index out of bounds");
}
template <typename T>
T &
DArenaVector<T>::insert(size_type pos, T && x) {
{
size_type new_z = size_ + 1;
size_type req_z = new_z * sizeof(T);
store_.expand(req_z, __PRETTY_FUNCTION__);
}
// move elements [i .. z-1] right by one position.
// must proceed in reverse order!
for (size_type ip1 = size_; ip1 > pos; --ip1) {
(*this)[ip1] = std::move((*this)[ip1-1]);
}
T * addr = this->_address_of(pos);
new (addr) T{std::move(x)};
this->size_ = size_ + 1;
return *addr;
}
template <typename T>
T &
DArenaVector<T>::insert(size_type pos, const T & x) {
{
size_type new_z = size_ + 1;
size_type req_z = new_z * sizeof(T);
store_.expand(req_z, __PRETTY_FUNCTION__);
}
// move elements [i .. z-1] right by one position.
// must proceed in reverse order!
for (size_type ip1 = size_; ip1 > pos; --ip1) {
(*this)[ip1] = std::move((*this)[ip1-1]);
}
T * addr = this->_address_of(pos);
new (addr) T{x};
this->size_ = size_ + 1;
return *addr;
}
template <typename T>
void
DArenaVector<T>::erase(size_type pos) {
// move elements [pos+1 .. z-1] left by one position.
if (pos >= size_) [[unlikely]]
return;
for (size_type i = pos; i+1 < size_; ++i) {
(*this)[i] = std::move((*this)[i+1]);
}
--(this->size_);
}
template <typename T>
void
DArenaVector<T>::push_back(T && x) {
size_type z = size_ + 1;
size_type req_z = z * sizeof(T);
if (this->store_.expand(req_z, __PRETTY_FUNCTION__)) {
T * addr = this->_address_of(size_);
new (addr) T{std::move(x)};
this->size_ = z;
}
}
template <typename T>
void
DArenaVector<T>::push_back(const T & x) {
size_type z = size_ + 1;
if (this->store_.expand(z * sizeof(T), __PRETTY_FUNCTION__)) {
T * addr = this->_address_of(size_);
new (addr) T{x};
this->size_ = z;
}
}
template <typename T>
void
DArenaVector<T>::swap(DArenaVector & other) noexcept {
std::swap(size_, other.size_);
std::swap(store_, other.store_);
}
} /*namespace mm*/
} /*namespace xo*/
/* end DArenaVector.hpp */

View file

@ -1,262 +0,0 @@
/** @file DCircularBuffer.hpp
*
* @author Roland Conybeare, Jan 2026
**/
#include "CircularBufferConfig.hpp"
#include "DArenaVector.hpp"
#include "hashmap/verify_policy.hpp"
#include "span.hpp"
#include <cstdint>
namespace xo {
namespace mm {
/** @class DCircularBuffer
*
* @brief high performance vm-aware circular buffer
*
* Circular buffer implementation with parsing-friendly performance features.
* - generalization of DArena.
* Like DArena, maps superpages as needed.
* Unlike DArena memory at the beginning of reserved range can be unmapped.
* - allows address range >> physical range
* - admits "Cheney on the MTA" strategy.
* May be feasible to reserve a lifetime address range (say 1TB)
* as long as buffer only every maps a subrange that fits in physical memory.
* - zero copy support for parsing / protocol trnaslation:
* provides capture/release semantics for a fixed number
* of remembered spans. Will never unmap memory for a remembered span,
* until that span is released.
* - automatically resets to beginning of reserved range
* whenever occupied range is empty
**/
struct DCircularBuffer {
public:
/** @defgroup mm-circularbuffer-types CircularBuffer type traits **/
///@{
/** an amount of memory **/
using size_type = std::size_t;
using byte = std::byte;
/** a contiguous addres range **/
using span_type = span<char>;
using const_span_type = span<const char>;
///@}
public:
/** @defgroup mm-cicrularbuffer-ctors CircularBuffer constructors **/
///@{
/** contruct instance
* @p config circular buffer configuration
* @p page_z o/s page size (via getpagesize())
* @p buffer_align_z alignment for buffer memory
* @p reserved_range reserved virtual address range
**/
DCircularBuffer(const CircularBufferConfig & config,
size_type page_z,
size_type buffer_align_z,
span_type reserved_range);
#ifdef NOT_YET
/** constructor */
DCircularBuffer(const CircularBufferConfig & config);
#endif
/** non-copyable **/
DCircularBuffer(const DCircularBuffer & other) = delete;
/** move ctor **/
DCircularBuffer(DCircularBuffer && other);
/**
* allocate virtual memory address (uncommitted!) for circular buffer
* with configuration @p config.
**/
static DCircularBuffer map(const CircularBufferConfig & config);
///@}
/** @defgroup mm-circularbuffer-const-methods CircularBuffer const methods **/
///@{
const_span_type reserved_range() const noexcept { return reserved_range_; }
const_span_type mapped_range() const noexcept { return mapped_range_; }
const_span_type occupied_range() const noexcept { return occupied_range_; }
const_span_type input_range() const noexcept { return input_range_; }
/** report memory-size info for this buffer to @p fn **/
void visit_pools(const MemorySizeVisitor & fn) const;
/** verify DCircularBuffer invariants.
* Act on failure according to policy @p p
* (combination of throw|log bits)
*
* verify invariants:
* CB1: mapped_range_ is subrange of reserved_range_
* CB2: occupied_range_ is subrange of mapped_range_
* CB3: each remembered_spans_[i] is subrange of occupied_range_
* CB4: buffer_align_z_ > 0 when buffer is mapped
* CB5: reserved_range_.lo() aligned on buffer_align_z_ boundary
**/
bool verify_ok(verify_policy p = verify_policy::throw_only()) const;
///@}
/** @defgroup mm-circularbuffer-nonconst-methods CircularBuffer non-const methods **/
///@{
span_type input_range() noexcept { return input_range_; }
/** copy memory in span @p r into buffer starting at the end of
* @ref occupied_range_. Map new physical memory as needed.
* On success returns empty suffix of @p r.
* If buffer memory exhausted, may copy a prefix of @p r.
* In that case returns the remaining suffix of @p r.
**/
const_span_type append(const_span_type r);
/** DMA version of @ref append_span : get mapped span A at which
* buffer will receive new content. Upstream may write into
* A. It must then coordinate with buffer by calling
* @ref report_append(P) for some prefix P of A
*
* Example:
* @code
* CircularBuffer buf = ...;
* constexpr size_type z = 64*1024;
* auto span = buf.get_append_span(z);
* ssize_t nr = read(FD, span.lo(), span.size());
* if (nr > 0)
* buf.report_append(span.prefix(nr));
* @endcode
**/
span_type get_append_span(size_type desired_z);
/** update bookkeeping as if caller had invoked append(r);
* however caller has already written to mapped memory
* after using get_append_span(); so omit copy
**/
void report_append(span_type r);
/** consume span (or prefix thereof) previously obtained from @ref occupied_range()
* Caller represents that it won't need to read this memory again
* unless overlaps with a pinned span.
**/
void consume(const_span_type input);
/** pin memory range @p r. circular buffer will not touch
* addresses that appear in any pinned range.
* use to
**/
void pin_range(span_type r);
/** unwind a previous pin_range call on range @p r.
* both start and end or @p r should exactly match a pinned range.
**/
void unpin_range(span_type r);
///@}
private:
/** @defgroup mm-circularbuffer-private-methods CircularBuffer non-const methods **/
///@{
/** expand hi end of mapped memory range to at least @p hi.
*
* Require: @p hi < @ref reserved_range_.hi
**/
bool _expand_to(char * hi);
/** shrink occupied rnage to the smallest contiguous range that contains both:
* all of .input_range_, and all pinned ranges in .pinned_spans_
**/
void _shrink_occupied_to_fit();
/** check for edge condition in which there are no pinned ranges. **/
void _check_reset_map_start();
///@}
private:
/** @defgroup mm-circularbuffer-instance-vars CircularBuffer member variables **/
///@{
/* memory layout
*
* reserved_range_ : entire address range owned by buffer (may be huge, e.g., 1TB)
* mapped_range_ : subrange backed by physical memory (fits in RAM)
* occupied_range_ : subrange currently containing data
* input_range_ : subrange containing unread input
* pinned_spans_ : pinned subranges within occupied (prevents alteration or unmap)
*
* <------------------- .reserved_range --------------------->
* . <------------- .mapped_range -------------> .
* . . <----- .occupied_range -----> . .
* . . . <- .input_range -----> . .
* . . . . . . .
* ........------XXXXXXXIIIIIIIIIIIIIIIIIIIIII--------........
* pp ppp pp
* Legend:
* [.] reserved : uncommitted memory. may be huge (e.g. 1TB)
* [-] mapped : range backed by physical memory
* [X] consumed : preserved until last overlapping pin removed
* [I] input : unread content, waiting to be read
* [p] pinned : pinned memory will not be altered (let alone unmapped)
*
* Invariants:
* - .input_range <= .occupied_range <= .mapped_range <= .reserved_range
* - mapped_range_ cannot shrink to exclude any portion of a pinned span
*/
/** buffer configuration **/
CircularBufferConfig config_;
/** size of a VM page (obtained automatically via getpagesize()). 4k on ubuntu. 16k on osx **/
size_type page_z_;
/** alignment for buffer address range.
* In practice will be either page_z_ or config_.hugepage_z_
**/
size_type buffer_align_z_;
/** Circular buffer owns address range defined by this span.
* Aligned on @ref buffer_align_z_.
* Always a whole number of @ref page_z_ or @ref config_.hugepage_z_
**/
span_type reserved_range_;
/** buffer owns memory defined by this span.
* Always a subrange of reserved_range
* These addresses backed by physical memory.
* Always a whole number of @ref page_z_ or @ref config_.hugepage_z_
**/
span_type mapped_range_;
/** currently occupied buffer memory.
* Always a subrange of @ref mapped_range_
**/
span_type occupied_range_;
/** portion of occupied buffer memory waiting to be read.
* Always represents a subspan of @ref occupied_range_, with the same
* hi endpoint.
* conversely @ref consume shrinks @ref input_range_ by increasing its lo endpoint.
**/
span_type input_range_;
/** remembered spans. For anticipated use cases expect one vm page sufficient.
* Spans in this vector always represent subranges of @ref occupied_range_
*
* @ref pinned_spans_ is confined to @ref occupied_range_.
* (In particular it's *not* confined to @ref input_range_)
*
* sorted on increasing span.lo()
**/
DArenaVector<span_type> pinned_spans_;
///@}
};
}
} /*namespace xo*/
/* end DCircularBuffer.hpp */

View file

@ -1,54 +0,0 @@
/** @file ErrorArena.hpp
*
* @author Roland Conybeare, Feb 2026
**/
#pragma once
#include "DArena.hpp"
namespace xo {
namespace mm {
/** @brief Dedicated arena for error reporting
*
* Reserving memory for error messaages.
* Motivation
* 1. so we have room to report an out-of-memory condition
* 2. so we have place to allocate for an error that
* doesn't interfere with other allocator state
*
* Expect to reset arena between errors, so only need
* enough room to report one error.
*
* To initialize explicitly:
* @code
* // before any other ErrorArena method calls:
* ErrorArena::init_once(cfg...);
*
* // do stuff with ErrorArena..
* ErrorArena::instance()
* @endcode
*
* Reminder: can't use obj<AAllocator> here,
* would be leveling violation.
**/
class ErrorArena {
public:
/** default configuration for error arena **/
static ArenaConfig default_config();
/** idempotent initialization **/
static void init_once(const ArenaConfig & cfg = default_config());
/** get initialized instnace **/
static DArena * instance();
private:
static DArena s_instance;
};
} /*namespace mm*/
} /*namespace xo*/
/* end ErrorArena.hpp */

View file

@ -1,71 +0,0 @@
/** @file MemorySizeInfo.hpp
*
* @author Roland Conybeare, Feb 2026
**/
#pragma once
#include <xo/reflectutil/typeseq.hpp>
#include <functional>
#include <string_view>
#include <cstddef>
namespace xo {
namespace mm {
struct MemorySizeDetail {
using typeseq = xo::reflect::typeseq;
/** identifies a c++ type T. See xo/facet/TypeRegistry **/
typeseq tseq_;
/** number of T-instances **/
uint32_t n_alloc_ = 0;
/** bytes used by T-instances **/
uint32_t z_alloc_ = 0;
};
struct MemorySizeInfo {
using size_type = std::size_t;
using DetailArrayType = std::array<MemorySizeDetail, 32>;
MemorySizeInfo() = default;
MemorySizeInfo(std::string_view name,
std::size_t u, std::size_t a, std::size_t c, std::size_t r,
const void * lo, const void * hi,
DetailArrayType * detail)
: resource_name_{name},
used_{u}, allocated_{a}, committed_{c}, reserved_{r}, lo_{lo}, hi_{hi}, detail_{detail}
{}
static MemorySizeInfo sentinel() { return MemorySizeInfo(); }
/** resource name **/
std::string_view resource_name_;
/** memory used (excluding wasted space) **/
std::size_t used_ = 0;
/** memory allocated (including wasted space e.g. empty slots in hash tables **/
std::size_t allocated_ = 0;
/** memory committed (backed by physical memory) **/
std::size_t committed_ = 0;
/** memory reserved:
* virtual memory addresses range obtained, whether or not committed
**/
std::size_t reserved_ = 0;
/** start address (optional) **/
const void * lo_ = 0;
/** end address (optional) **/
const void * hi_ = 0;
/** optional histogram with per-data-type counts **/
DetailArrayType * detail_ = nullptr;
};
/** function that visits MemorySizeInfo for a collection of @p n memory pools.
* Each pool reported with index @p i in [0, n), with associated
* size record @p info.
**/
using MemorySizeVisitor = std::function<void (const MemorySizeInfo & info)>;
}
}
/* end MemorySizeInfo.hpp */

View file

@ -1,236 +0,0 @@
/** @file arena_streambuf.hpp
*
* @author Roland Conybeare, Feb 2026
**/
#pragma once
#include "DArena.hpp"
//#include "print/quoted_char.hpp"
#include <iostream>
#include <string_view>
#include <vector>
#include <cstring> // e.g. for std::memcpy()
#include <cstdint>
#include <cassert>
namespace xo {
namespace mm {
/** @brief Arena-based buffer for logging and pretty-printing
*
* Arena-based using mmap
* Write to self-extending storage array
* Track position relative to start of line
**/
class arena_streambuf : public std::streambuf {
public:
struct rewind_state {
explicit rewind_state(std::size_t solpos, std::size_t color_esc, std::uint32_t p)
: solpos{solpos}, color_escape_chars{color_esc}, pos{p} {}
std::size_t solpos = 0;
std::size_t color_escape_chars = 0;
std::uint32_t pos = 0;
};
public:
/** arena should be ready-to-allocate i.e. have committed > 0 **/
arena_streambuf(DArena * arena, bool debug_flag = false) : arena_{arena}, debug_flag_{debug_flag} {
this->reset_stream();
} /*ctor*/
std::streamsize capacity() const { return arena_->committed(); }
const char * lo() const { return this->pbase(); }
const char * hi() const { return this->lo() + this->capacity(); }
std::uint32_t pos() const { return this->pptr() - this->pbase(); }
/** output position (relative to pbase) when local state last computed. Exposed here for unit tests **/
std::size_t _local_ppos() const { return local_ppos_; }
/** position (relative to pbase) one character after last \n or \r. For unit tests **/
std::uint32_t _solpos() const { return solpos_; }
/** start of incomplete color-escape sequence **/
const char * _color_escape_start() const { return color_escape_start_; }
/** number of non-printing chars after @ref solpos_ from completed color-escape sequences **/
std::uint32_t _color_escape_chars() const { return color_escape_chars_; }
/** number of visible characters since start of line (last \n or \r) **/
std::uint32_t lpos() const;
rewind_state checkpoint() const;
bool debug_flag() const { return debug_flag_; }
operator std::string_view () const { return std::string_view(this->pbase(), this->pptr()); }
void reset_stream();
void rewind_to(rewind_state s);
protected:
/** expand buffer storage (by 2x), preserve current contents **/
void expand_to(std::size_t new_z);
virtual std::streamsize xsputn(const char * s, std::streamsize n) override;
virtual int_type overflow(int_type new_ch) override;
/* off. offset, relative to starting point dir.
* dir.
* which. in|out|both
*
* Note that off=0,dir=cur,which=out reads offset
*/
virtual pos_type seekoff(off_type off,
std::ios_base::seekdir dir,
std::ios_base::openmode which) override;
private:
void _update_local_state_char(const char * p_lo, const char * p)
{
if ((*p == '\n') || (*p == '\r')) {
this->solpos_ = (p+1 - this->pbase());
/* reset, since these chars relevant as correction to solpos */
this->color_escape_chars_ = 0;
/* -> incomplete color escape, broken by newline */
this->color_escape_start_ = nullptr;
} else if (*p == '\033') {
if (debug_flag_) [[unlikely]] {
std::cout << "xsputn: \\033 at p-p_lo=" << (p - p_lo) << std::endl;
}
this->color_escape_start_ = p;
} else if (this->color_escape_start_ != nullptr) {
if (*p == 'm') {
/* escape seq non-printing including both endpoints */
std::int64_t esc_chars = (p+1 - color_escape_start_);
this->color_escape_chars_ += esc_chars;
if (debug_flag_) [[unlikely]] {
std::cout << "xsputn: m at p-p_lo" << (p - p_lo) << " +" << esc_chars
<< " -> color_escape_chars=" << color_escape_chars_ << std::endl;
}
this->color_escape_start_ = nullptr;
} else if (!isdigit(*p) && (*p != '[') && (*p != ';')) {
/* not color escape after all */
this->color_escape_start_ = nullptr;
}
}
}
/** recognize stale local state vars:
* @ref solpos_, @ref color_escape_chars_, @ref color_escape_start_.
*
* Require:
* - {pbase, pptr} in consistent state
* Promise:
* - @c local_ppos_ + @c pbase = @c pptr
* - @c solpos_, @c color_escape_chars_, @c color_escape_start_ all up-to-date
**/
void _check_update_local_state() {
const char * p0 = this->pbase();
const char * pn = this->pptr();
if (debug_flag_) {
std::cerr << "_check_update_local_state:" << std::endl;
std::cerr << " buf: (p0=" << (void*)p0 << ", pn=" << (void*)pn << ")" << std::endl;
std::cerr << " solpos_=" << solpos_ << ", color_escape_chars_=" << color_escape_chars_ << std::endl;
}
if (p0 + local_ppos_ == pn) [[likely]] {
// solpos_, color_escape_chars_, color_escape_start_ all up-to-date
} else {
// [pnew, pn): input that hasn't been incorporated into
// {solpos_, color_escape_chars_, color_escape_start_)
const char * pnew = this->pbase() + this->local_ppos_;
if (debug_flag_) {
std::cerr << "_check_update_local_state: range: (pnew=" << (void*)pnew << ", pn=" << (void*)pn << ")" << std::endl;
}
for(const char * p = pnew; p < pn; ++p) {
this->_update_local_state_char(p0, p);
}
}
// solpos_, color_escape_chars_, color_escape_start_ all up-to-date
// for current buffered contents
this->local_ppos_ = pn - p0;
if (debug_flag_) {
std::cerr << "_check_update_local_state: pos=" << pos();
std::cerr << ", solpos=" << solpos_;
std::cerr << ", color_escape_chars=" << color_escape_chars_ << std::endl;
}
assert(pos() >= solpos_ + color_escape_chars_);
}
private:
/*
* pbase: start of buffered text. Thils will be arena_->lo_
*
*
* pbase pptr epptr
* v >e1< >e2< v v
* |xx\xxEEExxx\xxxxxxxEExxxxEExxxxxxxEExxx\xEExxxxxx..................|
* ^ ^<------new------->
* solpos local_ppos
*
* solpos : first character after newline (stale)
* color_escape_pos : e1+e2+.. (stale)
* new : new characters not reflected
* in local_ppos_, color_escape_chars_ etc.
*
* Legend:
* [\] newline
* [x] visible character
* [E] color escape chars
*
*
* after _check_update_local_state():
*
*
* pbase pptr epptr
* v >e1< v v
* |xx\xxEEExxx\xxxxxxxEExxxxEExxxxxxxEExxx\xEExxxxxx..................|
* ^ ^
* solpos local_ppos
*
*/
/** @defgroup logstreambuf-instance-vars **/
///@{
/** value of pptr (relative to pbase) when _check_update_local_state() last ran **/
std::size_t local_ppos_ = 0;
/** position (relative to pbase) one character after last \n or \r.
* Use to drive @ref lpos. This _has_ to be lazy, since
* xsputn() isn'g guaranteed to be called when there's room in
* in buffer.
**/
std::size_t solpos_ = 0;
/** number of non-printing chars after @ref solpos_, from
* completed color escape sequences.
* (ansi color escapes = text between '\033' and 'm')
**/
std::size_t color_escape_chars_ = 0;
/** non-null: start of incomplete color escape sequence **/
const char * color_escape_start_ = nullptr;
/** buffered output stored here.
* We don't use arena's allocation api, just treat as a block of available memory
**/
DArena * arena_ = nullptr;;
/** true to debug log_streambuf itself **/
bool debug_flag_ = false;
///@}
}; /*log_streambuf*/
} /*namespace mm*/
} /*namespace xo*/
/* end arena_streambuf.hpp */

View file

@ -1,13 +0,0 @@
/** @file backtrace.hpp
*
* @author Roland Conybeare, Apr 2026
**/
#pragma once
namespace xo {
void print_backtrace(bool demangle_flag);
void print_backtrace_dwarf(bool demangle_flag);
}
/* end backtrace.hpp */

View file

@ -1,87 +0,0 @@
/** @file cmpresult.hpp
*
* @author Roland Conybeare, Dec 2025
**/
#pragma once
#include <iostream>
#include <cstdint>
namespace xo {
namespace mm {
enum class comparison : int32_t {
invalid = -1,
comparable = 0,
incomparable = +1,
};
extern const char * comparison2str(comparison x);
inline std::ostream &
operator<<(std::ostream & os, comparison x) {
os << comparison2str(x);
return os;
}
/** @brief result of a generic comparison operation
**/
struct cmpresult {
/** @defgroup mm-cmpresult-ctors cmpresult ctors **/
///@{
cmpresult() : err_{comparison::invalid}, cmp_{0} {}
cmpresult(comparison err, std::int16_t cmp) : err_{err}, cmp_{cmp} {}
static cmpresult incomparable() { return cmpresult(comparison::incomparable, 0); }
static cmpresult lesser() { return cmpresult(comparison::comparable, -1); }
static cmpresult equal() { return cmpresult(comparison::comparable, 0); }
static cmpresult greater() { return cmpresult(comparison::comparable, +1); }
template<typename T>
static cmpresult from_cmp(T && x, T && y) {
if (x < y)
return cmpresult::lesser();
else if (x == y)
return cmpresult::equal();
else
return cmpresult::greater();
}
///@}
/** @defgroup mm-cmpresult-methods cmpresult methods **/
///@{
/** print to stream **/
void display(std::ostream & os) const;
bool is_lesser() const {
return (err_ == comparison::comparable) && (cmp_ < 0);
}
bool is_equal() const {
return (err_ == comparison::comparable) && (cmp_ == 0);
}
///@}
/** @defgroup mm-cmpresult-instance-vars cmpresult instance vars **/
///@{
/** -1 -> invalid (sentinel)
* 0 -> comparable
* +1 -> incomparable (e.g. iterators from different arenas)
**/
comparison err_ = comparison::invalid;
/** <0 -> lesser; 0 -> equal, >0 -> greater **/
std::int16_t cmp_ = 0;
///@}
};
inline std::ostream & operator<<(std::ostream & os,
const cmpresult & x)
{
x.display(os);
return os;
}
} /*namespace mm*/
} /*namespace xo*/
/* end cmpresult.hpp */

View file

@ -1,83 +0,0 @@
/** @file ControlGroupo
*
* @author Roland Conybeare, Jan 2026
**/
#pragma once
#include "DArenaHashMapUtil.hpp"
#include <array>
#include <cstdint>
#include <cstring>
namespace xo {
namespace map {
namespace detail {
/** @brief 16x 8-bit control bytes.
*
* Support optimization using SIMD operations
**/
struct ControlGroup {
std::array<uint8_t, DArenaHashMapUtil::c_group_size> ctrl_;
/** Require: lo is aligned on c_group_size (probably 16 bytes) **/
explicit ControlGroup(const uint8_t * lo) {
::memcpy(ctrl_.data(), lo, DArenaHashMapUtil::c_group_size);
}
/** find all exact matches in ctrl_[0..15] for @p h2.
* for each match set corresponding bit in return value.
* Bits {0x1, 0x2, 0x4, ...} set iff exact match on
* {ctrl_[0], ctrl_[1], ctrl_2[], ...} respectively
**/
uint16_t all_matches(uint8_t h2) const {
uint16_t retval = 0;
uint16_t bit = 1;
for (auto xi : ctrl_) {
if (xi == h2)
retval |= bit;
bit = bit << 1;
}
return retval;
}
/** find all empty sentinels in ctrl_[0..15].
* for each empty, set corresponding bit in return value.
* Bits {0x1, 0x2, 0x4, ...} set iff empty spot
* {ctrl_[0], ctrl_[1], ctrl_[2], ...} respectively
**/
uint16_t empty_matches() const {
uint16_t retval = 0;
uint16_t bit = 1;
for (auto xi : ctrl_) {
if (xi == DArenaHashMapUtil::c_empty_slot)
retval |= bit;
bit = bit << 1;
}
return retval;
}
#ifdef NOT_YET
__m128i ctrl; // 16 bytes loaded via SSE2
// Find all slots matching h2
uint16_t Match(uint8_t h2) const {
__m128i pattern = _mm_set1_epi8(h2);
__m128i result = _mm_cmpeq_epi8(ctrl, pattern);
return _mm_movemask_epi8(result); // 16-bit mask
}
// Find all empty slots (0xFF)
uint16_t MatchEmpty() const {
return _mm_movemask_epi8(_mm_cmpeq_epi8(ctrl, _mm_set1_epi8(0xFF)));
}
#endif
};
}
} /*namespace map*/
} /*namespace xo*/
/* end ControlGroup.hpp */

View file

@ -1,153 +0,0 @@
/** @file DArenaHashMapIterator.hpp
*
* @author Roland Conybeare, Jan 2026
**/
#pragma once
#include <xo/arena/hashmap/DArenaHashMapUtil.hpp>
namespace xo {
namespace map {
namespace detail {
template <typename Key,
typename Value>
struct DArenaHashMapIterator : public DArenaHashMapUtil {
using value_type = std::pair<const Key, Value>;
public:
DArenaHashMapIterator(uint8_t * c, value_type * p)
: ctrl_{c}, pos_{p} {}
value_type & operator*() const { return *pos_; }
value_type * operator->() const { return pos_; }
uint8_t * _ctrl() const { return ctrl_; }
value_type * _pos() const { return pos_; }
/** true iff iterator at sentinel position (not dereferencable state !) **/
bool _at_slot_sentinel() const { return is_sentinel(*ctrl_) && (*ctrl_ != c_iterator_bookend); }
bool operator==(const DArenaHashMapIterator & x) const {
return this->pos_ == x.pos_;
}
bool operator!=(const DArenaHashMapIterator & x) const {
return this->pos_ != x.pos_;
}
DArenaHashMapIterator & operator++() {
do {
++(this->ctrl_);
++(this->pos_);
/** end condition: iterator ends at last non-wrapped position.
* relyin on bookend sentinel values at known offset from 'wrap' section
*
* ctrl_ ctrl_ + c_group_size
* | |
* v v
* <----------------- control_size(n_slot) ---------------->
* <-stub-> <----------- n_slot ----------> <group> <-stub->
* +--------+-------------------------------+-------+--------+
* | 0xF0 | empty / data / tombstone | wrap | 0xF0 |
* +--------+-------------------------------+-------+--------+
**/
} while (is_sentinel(*ctrl_)
&& (*(ctrl_ + c_group_size) != c_iterator_bookend));
return *this;
}
DArenaHashMapIterator & operator--() {
/* simpler than forward iteration, since bookend immediately
* precedes control byte for first slot
*/
do {
--(this->ctrl_);
--(this->pos_);
} while (is_sentinel(*ctrl_)
&& (*ctrl_ != c_iterator_bookend));
return *this;
}
private:
uint8_t * ctrl_ = nullptr;
value_type * pos_ = nullptr;
};
template <typename Key,
typename Value>
struct DArenaHashMapConstIterator : public DArenaHashMapUtil {
using value_type = std::pair<const Key, Value>;
public:
DArenaHashMapConstIterator(const uint8_t * c, const value_type * p)
: ctrl_{c}, pos_{p} {}
const value_type & operator*() const { return *pos_; }
const value_type * operator->() const { return pos_; }
const uint8_t * _ctrl() const { return ctrl_; }
const value_type * _pos() const { return pos_; }
/** true iff iterator at sentinel position (not dereferencable state !) **/
bool _at_slot_sentinel() const {
return is_sentinel(*ctrl_) && (*ctrl_ != c_iterator_bookend);
}
bool operator==(const DArenaHashMapConstIterator & x) const {
return this->pos_ == x.pos_;
}
bool operator!=(const DArenaHashMapConstIterator & x) const {
return this->pos_ != x.pos_;
}
DArenaHashMapConstIterator & operator++() {
do {
++(this->ctrl_);
++(this->pos_);
/** end condition: iterator ends at last non-wrapped position.
* relyin on bookend sentinel values at known offset from 'wrap' section
*
* ctrl_ ctrl_ + c_group_size
* | |
* v v
* <----------------- control_size(n_slot) ---------------->
* <-stub-> <----------- n_slot ----------> <group> <-stub->
* +--------+-------------------------------+-------+--------+
* | 0xF0 | empty / data / tombstone | wrap | 0xF0 |
* +--------+-------------------------------+-------+--------+
**/
} while (is_sentinel(*ctrl_)
&& (*(ctrl_ + c_group_size) != c_iterator_bookend));
return *this;
}
DArenaHashMapConstIterator & operator--() {
/* simpler than forward iteration, since bookend immediately
* precedes control byte for first slot
*/
do {
--(this->ctrl_);
--(this->pos_);
} while (is_sentinel(*ctrl_)
&& (*ctrl_ != c_iterator_bookend));
return *this;
}
private:
const uint8_t * ctrl_ = nullptr;
const value_type * pos_ = nullptr;
};
}
} /*namespace map*/
} /*namespace xo*/
/* end DArenaHashMapIterator.hpp */

View file

@ -1,117 +0,0 @@
/** @file DArenaHashMapUtil.hpp
*
* @author Roland Conybeare, Jan 2026
**/
#pragma once
#include <cstdint>
namespace xo {
namespace map {
/** @class DArenaHashMapUtil
*
* @pre
*
* control
*
* <----------------- control_size(n_slot) ---------------->
* <-stub-> <----------- n_slot ----------> <group> <-stub->
* +--------+-------------------------------+-------+--------+
* | 0xF0 | empty / data / tombstone | wrap | 0xF0 |
* +--------+-------------------------------+-------+--------+
* ^ ^
* | ... | control_[stub+i] <--> slots_[i]
* slots v v
* +-------------------------------+
* | {k,v} pairs |
* +-------------------------------+
* <--- n_slot key-value pairs -->
*
* sizes:
* - stub before+after bookends. c_control_stub bytes (16)
* - group c_group_size. power of 2 (16 bytes)
* - n_slot hash table slots. power of 2 multiple of c_group_size.
*
* control bytes:
* - 0b1xxxxxxx sentinel bitmask
* - 0xf0 sentinel for before/after stubs (iterator bookends)
* - 0xff sentinel for empty slot.
* - 0xfe sentinel for tombstone
* - 0b0xxxxxxx high bit clear; remainder hold low 7 bits of hash
* - wrap duplicate first c_group_size bytes (after front stub)
* for SIMD convenience
*
* @endpre
**/
struct DArenaHashMapUtil {
using size_type = std::size_t;
using control_type = std::uint8_t;
/** control: mask for sentinel states **/
static constexpr uint8_t c_sentinel_mask = 0x80;
/** control: sentinel for empty slot **/
static constexpr uint8_t c_empty_slot = 0xFF;
/** control: tombstone for deleted slot **/
static constexpr uint8_t c_tombstone = 0xFE;
/** control: bookends around control array,
* for iterator edge support
**/
static constexpr uint8_t c_iterator_bookend = 0xF0;
/** group size **/
static constexpr size_type c_group_size = 16;
/** max load factor **/
static constexpr float c_max_load_factor = 0.875;
/** Iterator sentinel at begin/end of control array.
* Load-bearing for bidirectional iterator implementation
**/
static constexpr size_type c_control_stub = c_group_size; //c_group_size;
/** control: true for sentinel values **/
static constexpr bool is_sentinel(control_type ctrl) {
return c_sentinel_mask == (ctrl & c_sentinel_mask);
}
/** control; true for non-sentinel values **/
static constexpr bool is_data(control_type ctrl) {
return 0 == (ctrl & c_sentinel_mask);
}
/** control: compute size of control array for swiss hash map with @p n_slot cells **/
static constexpr size_type control_size(size_type n_slot) {
if (n_slot == 0)
return 0;
/* control:
* - c_group_size overflow slots
* - 2x c_control_stub begin/end sentinels
*/
return n_slot + c_group_size + (2 * c_control_stub);
}
/** find smallest multiple k : k * c_group_size >= n **/
static size_type lub_group_mult(size_t n) {
return (n + c_group_size - 1) / c_group_size;
}
/** find smallest x such that 2^x >= n. Return {x, 2^x} **/
static std::pair<size_type, size_type> lub_exp2(size_t n) {
size_type ngx = 0;
size_type ng = 1;
while (ng < n) {
++ngx;
ng *= 2;
}
return std::make_pair(ngx, ng);;
}
};
} /*namespace map*/
} /*namespace xo*/
/* end DArenaHashMapUtil.hpp */

View file

@ -1,163 +0,0 @@
/** @file HashMapStore.hpp
*
* @author Roland Conybeare, Jan 2026
**/
#pragma once
#include <xo/arena/hashmap/DArenaHashMapUtil.hpp>
#include <xo/arena/hashmap/ControlGroup.hpp>
namespace xo {
namespace map {
namespace detail {
template <typename Key,
typename Value>
struct HashMapStore : DArenaHashMapUtil {
public:
using value_type = std::pair<const Key, Value>;
using group_type = detail::ControlGroup;
using control_vector_type = xo::mm::DArenaVector<uint8_t>;
using slot_vector_type = xo::mm::DArenaVector<value_type>;
using MemorySizeVisitor = xo::mm::MemorySizeVisitor;
using MemorySizeInfo = xo::mm::MemorySizeInfo;
public:
/** group_exp2: number of groups {x, 2^x} **/
explicit HashMapStore(const std::string & name,
const std::pair<size_type,
size_type> & group_exp2)
: size_{0},
n_group_exponent_{group_exp2.first},
n_group_{group_exp2.second},
n_slot_{group_exp2.second * c_group_size},
control_{control_vector_type::map
(xo::mm::ArenaConfig{
.name_ = name + "-ctl",
.size_ = control_size(n_slot_),
.store_header_flag_ = false})},
slots_{slot_vector_type::map
(xo::mm::ArenaConfig{
.name_ = name + "-slots",
.size_ = n_slot_ * sizeof(value_type),
.store_header_flag_ = false})}
{
/* here: arenas have allocated address range, but no committed memory yet */
this->_init();
}
size_type empty() const noexcept { return size_ == 0; }
size_type capacity() const noexcept { return n_group_ * c_group_size; }
float load_factor() const noexcept { return size_ / static_cast<float>(n_slot_); }
void visit_pools(const MemorySizeVisitor & visitor) const {
// complexity here in service of HashMapStore-specific value for MemorySizeInfo.used
MemorySizeInfo ctl_info;
MemorySizeInfo slot_info;
control_.visit_pools([&ctl_info](const auto & x) { ctl_info = x; });
slots_.visit_pools([&slot_info](const auto & x) { slot_info = x; });
// control: 1 byte per (key,value) pair
ctl_info.used_ = size_;
slot_info.used_ = size_ * sizeof(value_type);
visitor(ctl_info);
visitor(slot_info);
}
void resize_from_empty(const std::pair<size_type,
size_type> & group_exp2)
{
assert(size_ == 0);
this->n_group_exponent_ = group_exp2.first;
this->n_group_ = group_exp2.second;
this->n_slot_ = group_exp2.second * c_group_size;
this->_init();
}
void clear() {
/* remark: discontinuity in the sense that we lose n_group_ = 2 ^ n_group_epxonent_
*
* juice may not be worth the squeeze here,
* since DArena doesn't yet (Jan 2026) unmap on clear
*/
this->size_ = 0;
this->n_group_exponent_ = 0;
this->n_group_ = 0;
this->n_slot_ = 0;
this->control_.resize(0);
this->slots_.resize(0);
}
public:
void _init() {
this->control_.resize(control_size(n_slot_));
/* front stub: iterator bookend */
std::fill(this->control_.begin(),
this->control_.begin() + c_control_stub,
c_iterator_bookend);
/* all slots marked empty initially */
std::fill(this->control_.begin() + c_control_stub,
this->control_.end() - c_control_stub,
c_empty_slot);
/* end stub: iterator bookend */
std::fill(this->control_.end() - c_control_stub,
this->control_.end(),
c_iterator_bookend);
this->slots_.resize(n_slot_);
}
/** load control group for slot range [ix .. ix+c_group_size) **/
group_type _load_group(size_type ix) const {
return group_type(&(control_[ix + c_control_stub]));
}
/** update control group for slot number @p ix, replace with @p h2 **/
void _update_control(size_type ix, uint8_t h2) {
this->control_[ix + c_control_stub] = h2;
if (ix < c_group_size) {
size_type N = this->capacity();
// refresh end-of-array copy
std::memcpy(&(control_[N + c_control_stub]),
&(control_[c_control_stub]),
c_group_size);
}
}
public:
/** number of pairs in this table **/
size_type size_ = 0;
/** base-2 logarithm of n_group_ **/
size_type n_group_exponent_ = 0;
/** table has capacity for this number of groups.
* always an exact power of two.
* number of slots is n_group_ * c_group_size
**/
size_type n_group_ = (1 << n_group_exponent_);
/** table has capacity for this number of {key,value} pairs **/
size_type n_slot_ = n_group_ * c_group_size;
/** control_[] partitioned into groups of
* c_group_size (16) consecutive elements
**/
control_vector_type control_;
/** slots_[] holds {key,value} pairs **/
slot_vector_type slots_;
};
}
} /*namespace map*/
} /*namespace xo*/
/* end HashMapStore.hpp */

View file

@ -1,59 +0,0 @@
/** @file verify_policy.hpp
*
* @author Roland Conybeare, Jan 2026
**/
#pragma once
#include <xo/indentlog/scope.hpp>
#include <string>
namespace xo {
// TODO: move xo/indentlog
/** @brief policy for verify_ok behavior.
*
* Remarke: wrote this for DArenaHashMap,
* want to incorporate into other subsystems
* that provide a verify_ok() method.
* e.g. RedBlackTree
**/
struct verify_policy {
static verify_policy log_only() {
return verify_policy{.flags_ = 0x01};
}
static verify_policy throw_only() {
return verify_policy{.flags_ = 0x02};
}
static verify_policy chatty() {
return verify_policy{.flags_ = 0x03};
}
bool is_silent() const noexcept { return flags_ == 0; }
bool log_flag() const noexcept { return flags_ & 0x01; }
bool throw_flag() const noexcept { return flags_ & 0x02; }
template<typename... Tn>
bool report_error(scope & log, Tn&&... args)
{
if (!this->is_silent()) {
// TODO: consider global arena here for string
std::string msg = tostr(std::forward<Tn>(args)...);
if (this->log_flag()) {
log.retroactively_enable();
log(msg);
}
if (this->throw_flag()) {
throw std::runtime_error(msg);
}
}
return false;
}
const char * c_self_ = "anonymous";
uint8_t flags_;
};
} /*namespace xo*/
/* end verify_policy.hpp */

View file

@ -1,49 +0,0 @@
/** @file mmap_util.hpp
*
* @author Roland Conybeare, Jan 2026
**/
#pragma once
#include "span.hpp"
namespace xo {
namespace mm {
struct mmap_util {
using byte = std::byte;
using span_type = span<byte>;
using size_type = std::size_t;
/** obtain uncommitted contiguous memory range comprising
* a whole multiple of @p align_z bytes, of at least size @p req_z,
* aligned on a @p align_z boundary. Uncommitted memory is not (yet)
* backed by physical memory.
*
* If @p enable_hugepage_flag is true and THP
* (transparent huge pages) are available, use THP for arena memory.
* This relieves TLB and page table memory when @p req_z is a lot larger than
* page size (likely 4KB). Cost is that arena will consum physical memory in unit
* of @p align_z. Arena may waste up to @p align_z bytes of memory as a result.
*
* If @p enable_hugepage_flag is true, @p align_z should be huge page size
* (probably 2MB) for optimal performance.
*
* At present the THP feature is not supported on OSX.
* May be supportable through mach_vm_allocate().
*
* Note that we reject MAP_HUGETLB|MAP_HUGE_2MB flags to mmap here,
* since requires previously-reserved memory in /proc/sys/vm/nr_hugepages.
*
* Write log messages iff @p debug_flag is true.
*
* @return spqn giving reserved memory address range [lo,hi)
**/
static span_type map_aligned_range(size_type req_z,
size_type align_z,
bool enable_hugepage_flag,
bool debug_flag);
};
} /*namespace mm*/
} /*namespace xo*/
/* end mmap_util.hpp */

View file

@ -1,60 +0,0 @@
/** @file padding.hpp
*
* @author Roland Conybeare, Dec 2025
**/
#pragma once
#include <memory>
#include <cstdint>
namespace xo {
namespace mm {
struct padding {
/** word size for alignment**/
static constexpr std::size_t c_alloc_alignment = sizeof(std::uintptr_t);
static inline std::size_t is_aligned(std::size_t n,
std::size_t align = c_alloc_alignment) {
return n % align == 0;
}
/** how much to add to @p z to get a multiple of
* @ref c_alloc_alignment
**/
static inline std::size_t alloc_padding(std::size_t z,
std::size_t align = c_alloc_alignment)
{
/* round up to multiple of c_bpw, but map 0 -> 0
* (table assuming c_bpw==8)
*
* z%c_bpw dz
* ------------
* 0 0
* 1 7
* 2 6
* .. ..
* 7 1
*/
std::size_t dz = (align - (z % align)) % align;
return dz;
}
/** @p z rounded up to an exact multiple
* of @ref c_alloc_alignment
**/
static inline
std::size_t with_padding(std::size_t z,
std::size_t align = c_alloc_alignment)
{
return z + alloc_padding(z, align);
}
};
} /*namespace mm*/
} /*namespace xo*/
/* end padding.hpp */

View file

@ -1,39 +0,0 @@
/** @file print.hpp
*
* @author Roland Conybeare, Dec 2025
**/
#pragma once
#include "AllocError.hpp"
#include <xo/indentlog/print/tag.hpp>
#include <iostream>
namespace xo {
namespace mm {
inline std::ostream &
operator<<(std::ostream & os, const error & x) {
os << AllocError::error_description(x);
return os;
}
inline std::ostream &
operator<<(std::ostream & os, const AllocError & x) {
os << "<AllocError"
<< xtag("error", x.error_);
if (x.src_fn_)
os << xtag("src_fn", x.src_fn_);
os << xtag("seq", x.error_seq_)
<< xtag("req_z", x.request_z_)
<< xtag("commit_z", x.committed_z_)
<< xtag("resv_z", x.reserved_z_)
<< ">";
return os;
}
}
}
/* end print.hpp */

View file

@ -1,329 +0,0 @@
/** @file span.hpp
*
* @author Roland Conybeare, Jul 2024
**/
#pragma once
#include "xo/indentlog/scope.hpp"
#include "xo/indentlog/print/ppdetail_atomic.hpp"
#include <ostream>
#include <cstdint>
#include <cassert>
namespace xo {
namespace mm {
/** @class span compression/span.hpp
*
* @brief A contiguous range of characters, without ownership.
*
* @tparam CharT type for elements referred to by this span.
**/
template <typename CharT>
class span {
public:
/** @defgroup span-type-traits span type traits **/
///@{
/** typealias for span size (in units of CharT) **/
using size_type = std::uint64_t;
/** typealias for span elements **/
using value_type = CharT;
///@}
public:
/** @defgroup span-ctors span constructors **/
///@{
/** null span **/
span() : lo_{nullptr}, hi_{nullptr} {}
/** Create span for the contiguous memory range [@p lo, @p hi) **/
span(CharT * lo, CharT * hi) : lo_{lo}, hi_{hi} {}
/** Create span for the contiguous memory range [@p lo, @p lo + z) **/
span(CharT * lo, size_t z) : lo_{lo}, hi_{lo + z} {}
/** explicit conversion from span<U> **/
template<typename CharU>
span(const span<CharU> & other,
std::enable_if_t<std::is_convertible_v<CharU*, CharT*>
&& !std::is_same_v<CharU, CharT>> * = nullptr)
: lo_{other.lo()}, hi_{other.hi()} {}
/** copy ctor (explicit to avoid ambiguity with template ctor) **/
span(const span & other) = default;
span & operator=(const span & other) = default;
/** Create a null span (i.e. with null @p lo, @p hi pointers)
* A null span can be concatenated with any other span
* without triggering matching-endpoint asserts.
**/
static span make_null() { return span(static_cast<CharT*>(nullptr),
static_cast<CharT*>(nullptr)); }
/** @brief create span for C-style string @p cstr **/
static span from_cstr(const CharT * cstr) {
CharT * lo = cstr;
CharT * hi = cstr ? cstr + strlen(cstr) : nullptr;
return span(lo, hi);
}
/** @brief create span from std::string @p str **/
static span from_string(const std::string & str) {
CharT * lo = &(*str.begin());
CharT * hi = &(*str.end());
return span(lo, hi);
}
/** @brief create span from std::string @p str **/
static span from_string_view(const std::string_view & sv) {
CharT * lo = &(*sv.begin());
CharT * hi = &(*sv.end());
return span(lo, hi);
}
/** @brief create span from raw memory **/
static span from_memory(span<std::byte> span_memory) {
CharT * lo = (CharT *)span_memory.lo();
CharT * hi = (CharT *)span_memory.hi();
return span(lo, hi);
}
/** @brief concatenate two contiguous spans */
static span concat(const span & span1, const span & span2) {
if (span1.is_null())
return span2;
if (span2.is_null())
return span1;
if (span1.hi() != span2.lo()) {
scope log(XO_DEBUG(true));
log && log(xtag("span1.hi", (void*)span1.hi()), xtag("span2.lo", (void*)span2.lo()));
}
assert(span1.hi() == span2.lo());
CharT * lo = span1.lo();
CharT * hi = span2.hi();
return span(lo, hi);
}
///@}
/** @defgroup span-access-methods **/
///@{
CharT * lo() const { return lo_; } /* get member span::lo_ */
CharT * hi() const { return hi_; } /* get member span::hi_ */
/** true iff this span is null. distinct from empty. **/
bool is_null() const { return lo_ == nullptr && hi_ == nullptr; }
/** true iff this span is empty (comprises 0 elements). **/
bool empty() const { return lo_ == hi_; }
/** report the number of elements (of type CharT) in this span. **/
size_type size() const { return hi_ - lo_; }
/** true iff this span is a subspan of @p other.
* i.e. other.lo() <= this->lo() && this->hi() <= other.hi()
**/
bool is_subspan_of(const span & other) const noexcept {
return (other.lo() <= lo_) && (hi_ <= other.hi());
}
/** convert to string view **/
std::string_view to_string_view() const {
return std::string_view((const char *)lo_, (const char *)hi_);
}
///@}
/** @defgroup span-general-methods **/
///@{
/** @brief strip prefix until first occurence of '\n', including the newline **/
void discard_until_newline() {
for (const CharT * p = lo_; p < hi_; ++p) {
if (*p == '\n') {
lo_ = p + 1;
return;
}
}
lo_ = hi_;
}
/** Create new span over supplied type,
* with identical (possibly misaligned) endpoints.
*
* @warning
* 1. New span uses exactly the same memory addresses.
* Endpoint pointers may not be aligned.
* 2. Implementation assumes code compiled with
* @code -fno-strict-aliasing @endcode enabled.
*
* @tparam OtherT element type for new span
**/
template <typename OtherT>
span<OtherT>
cast() const { return span<OtherT>(reinterpret_cast<OtherT *>(lo_),
reinterpret_cast<OtherT *>(hi_)); }
/** @brief create span including the first @p z members of this span. **/
span prefix(size_type z) const { return span(lo_, lo_ + z); }
/** @brief create span representing prefix up to (but not including) @p *p
**/
span prefix_upto(CharT * p) const {
if (p <= hi_)
return span(lo_, p);
else
return span(lo_, hi_);
}
/** @brief create span with first @p z members of this span removed **/
span after_prefix(size_type z) const {
if (lo_ + z > hi_)
z = hi_ - lo_;
return span(lo_ + z, hi_);
}
/** @brief create span with @p prefix of this span removed **/
span after_prefix(const span & prefix) const {
if (!prefix.is_null() && (prefix.lo() != lo_)) {
throw std::runtime_error
("after_prefix: expected prefix of this span");
}
return after_prefix(prefix.size());
}
/** Create span starting with position @p p.
* Does boundary checking; will return empty span if @p p is outside @c [lo_,hi)
**/
span suffix_from(CharT * p) const {
if ((lo_ <= p) && (p <= hi_))
return span(p, hi_);
else
return span(hi_, hi_);
}
/** increase extent of this spans to include @p x.
* Requires @c hi() == @c x.lo()
**/
span & operator+=(const span & x) {
if (hi_ == x.lo_) {
hi_ = x.hi_;
} else if (!x.is_null()) {
assert(false);
}
return *this;
}
/** print representation for this span on stream @p os **/
void print(std::ostream & os) const {
os << "<span"
<< xtag("addr", (void*)lo_)
<< xtag("size", size())
<< " :text " << xo::print::quot(std::string_view(lo_, hi_))
<< ">";
}
///@}
private:
/** @defgroup span-instance-vars **/
///@{
/** start of span.
Span comprises memory address between @p lo (inclusive) and @p hi (exclusive)
**/
CharT * lo_ = nullptr;
/** @brief end of span.
Span comprises memory address between @p lo (inclusive) and @p hi (exclusive)
**/
CharT * hi_ = nullptr;
///@}
}; /*span*/
/** @defgroup span-operators **/
///@{
/** compare spans for equality.
* Two spans are equal iff both endpoints match exactly.
**/
template <typename CharT>
inline bool
operator==(const span<CharT> & lhs, const span<CharT> & rhs) {
return ((lhs.lo() == rhs.lo())
&& (lhs.hi() == rhs.hi()));
}
/** compare spans for inequality.
* Two spans are unequal if either paired endpoint differs.
**/
template <typename CharT>
inline bool
operator!=(const span<CharT> & lhs, const span<CharT> & rhs) {
return ((lhs.lo() != rhs.lo())
|| (lhs.hi() != rhs.hi()));
}
/** print a summary of @p x on stream @p os. Intended for diagnostics **/
template <typename CharT>
inline std::ostream &
operator<<(std::ostream & os,
const span<CharT> & x) {
x.print(os);
return os;
}
///@}
} /*namespace scm*/
namespace print {
template <typename CharT>
class printspan_impl {
public:
printspan_impl(xo::mm::span<CharT> x) : span_{x} {}
xo::mm::span<CharT> span_;
};
template <typename CharT>
printspan_impl<CharT> printspan(const xo::mm::span<CharT>& span) {
return printspan_impl<CharT>(span);
}
template <typename CharT>
inline std::ostream &
operator<< (std::ostream & os,
const printspan_impl<CharT> & x)
{
for (const CharT * p = x.span_.lo(); p < x.span_.hi(); ++p)
os << *p;
return os;
}
#ifndef ppdetail_atomic
template <typename CharT> \
PPDETAIL_ATOMIC_BODY(printspan_impl<CharT>);
template <typename CharT> \
PPDETAIL_ATOMIC_BODY(xo::scm::span<CharT>);
#endif
} /*namespace mm*/
} /*namespace xo*/

View file

@ -1,45 +0,0 @@
/** @file AllocError.cpp
*
* @author Roland Conybeare, Dec 2025
**/
#include "AllocError.hpp"
namespace xo {
namespace mm {
const char *
AllocError::error_description(error x)
{
switch (x) {
case error::invalid:
break;
case error::ok:
return "ok";
case error::reserve_exhausted:
return "reserve-exhausted";
case error::commit_failed:
return "commit-failed";
case error::header_size_mask:
return "header-size-mask";
case error::orphan_sub_alloc:
return "orphan-sub-alloc";
case error::alloc_info_disabled:
return "alloc-info-disabled";
case error::alloc_info_address:
return "alloc-info-address";
case error::alloc_iterator_not_supported:
return "alloc-iterator-not-supported";
case error::alloc_iterator_deref:
return "alloc-iterator-deref";
case error::alloc_iterator_next:
return "alloc-iterator-next";
}
return "?error";
}
} /*namespace mm*/
} /*namespace xo*/
/* end AllocError.cpp */

View file

@ -1,45 +0,0 @@
/** @file AllocInfo.cpp
*
* @author Roland Conybeare, Dec 2025
**/
#include "AllocInfo.hpp"
namespace xo {
namespace mm {
auto
AllocInfo::guard_lo() const noexcept -> span_type
{
if (!p_guard_lo_)
return span_type(nullptr, nullptr);
return span_type(p_guard_lo_,
p_guard_lo_ + p_config_->guard_z_);
}
auto
AllocInfo::payload() const noexcept -> span_type
{
if (!p_header_)
return span_type(nullptr, nullptr);
byte * lo = (byte *)(p_header_ + 1);
size_type z = this->size();
return span_type(lo, lo+z);
}
auto
AllocInfo::guard_hi() const noexcept -> span_type
{
if (!p_guard_hi_)
return span_type(nullptr, nullptr);
return span_type(p_guard_hi_,
p_guard_hi_ + p_config_->guard_z_);
}
} /*namespace mm*/
} /*namespace xo*/
/* end AllocInfo.cpp */

View file

@ -1,35 +0,0 @@
# xo-arena/src/CMakeLists.txt
set(SELF_LIB xo_arena)
set(SELF_SRCS
arena_streambuf.cpp
ErrorArena.cpp
cmpresult.cpp
mmap_util.cpp
AllocError.cpp
AllocInfo.cpp
DArena.cpp
DArenaIterator.cpp
DCircularBuffer.cpp
backtrace.cpp
)
xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS})
xo_install_include_tree3(include/xo/arena)
# ----------------------------------------------------------------
# input dependencies
#
# NOTE: dependency set here must be kept consistent with
# xo-arena/cmake/xo_arenaConfig.cmake.in
xo_dependency(${SELF_LIB} xo_reflectutil)
xo_dependency(${SELF_LIB} indentlog)
if (NOT APPLE)
xo_external_pkgconfig_dependency(${SELF_LIB} LIBUNWIND libunwind-generic)
xo_external_pkgconfig_dependency(${SELF_LIB} LIBDW libdw)
else()
endif()
# end src/CMakeLists.txt

View file

@ -1,655 +0,0 @@
/** @file DArena.cpp
*
* @author Roland Conybeare, Dec 2025
**/
//#include "alloc/AAllocator.hpp"
#include "DArena.hpp"
#include "DArenaIterator.hpp"
#include "mmap_util.hpp"
#include "backtrace.hpp"
#include <xo/arena/padding.hpp>
#include <xo/indentlog/scope.hpp>
#include <xo/indentlog/print/tag.hpp>
#include <cassert>
#include <exception>
#include <new> // for std::launder()
#include <sys/mman.h> // for ::munmap()
#include <unistd.h> // for ::getpagesize()
#include <string.h> // for ::memset()
namespace xo {
using xo::reflect::typeseq;
using std::byte;
using std::cerr;
using std::endl;
using std::size_t;
namespace mm {
DArena
DArena::map(const ArenaConfig & cfg)
{
scope log(XO_DEBUG(cfg.debug_flag_));
/* vm page size. 4KB, probably */
size_t page_z = getpagesize();
bool enable_hugepage_flag = (cfg.size_ >= cfg.hugepage_z_);
/* Align start of arena memory on this boundary.
* Will use THP (transparent huge pages) if available
* and arena size is at least as large as hugepage size (2MB, probably)
*/
size_t align_z = (enable_hugepage_flag ? cfg.hugepage_z_ : page_z);
log && log(xtag("page_z", page_z),
xtag("align_z", align_z));
auto span = mmap_util::map_aligned_range(cfg.size_,
align_z,
enable_hugepage_flag,
cfg.debug_flag_);
if (!span.lo()) {
// control here implies mmap() failed silently
throw std::runtime_error(tostr("ArenaAlloc: reserve address range failed",
xtag("size", cfg.size_)));
}
#ifdef NOPE
log && log(xtag("lo", (void*)lo_),
xtag("page_z", page_z_),
xtag("hugepage_z", hugepage_z_));
#endif
return DArena(cfg, page_z, align_z, span.lo(), span.hi());
} /*map*/
DArena::DArena(const ArenaConfig & cfg)
{
*this = map(cfg);
}
DArena::DArena(const ArenaConfig & cfg,
size_type page_z,
size_type arena_align_z,
byte * lo,
byte * hi) : config_{cfg},
page_z_{page_z},
arena_align_z_{arena_align_z},
lo_{lo},
committed_z_{0},
free_{lo},
limit_{lo},
hi_{hi},
error_count_{0},
last_error_{}
{
//retval.checkpoint_ = lo_;
/** make sure guard size is aligned **/
config_.header_.guard_z_
= padding::with_padding(config_.header_.guard_z_);
}
DArena::DArena(DArena && other) {
config_ = other.config_;
page_z_ = other.page_z_;
arena_align_z_ = other.arena_align_z_;
lo_ = other.lo_;
committed_z_ = other.committed_z_;
free_ = other.free_;
limit_ = other.limit_;
hi_ = other.hi_;
error_count_ = other.error_count_;
last_error_ = other.last_error_;
other.config_ = ArenaConfig();
other.lo_ = nullptr;
other.committed_z_ = 0;
other.free_ = nullptr;
other.limit_ = nullptr;
other.hi_ = nullptr;
other.error_count_ = 0;
other.last_error_ = AllocError();
}
DArena &
DArena::operator=(DArena && other)
{
config_ = other.config_;
page_z_ = other.page_z_;
arena_align_z_ = other.arena_align_z_;
lo_ = other.lo_;
committed_z_ = other.committed_z_;
free_ = other.free_;
limit_ = other.limit_;
hi_ = other.hi_;
error_count_ = other.error_count_;
last_error_ = other.last_error_;
other.config_ = ArenaConfig();
other.lo_ = nullptr;
other.committed_z_ = 0;
other.free_ = nullptr;
other.limit_ = nullptr;
other.hi_ = nullptr;
other.error_count_ = 0;
other.last_error_ = AllocError();
return *this;
}
void
DArena::unmap() noexcept
{
if (lo_) {
//log && log("unmap [lo,hi)",
// xtag("lo", lo_),
// xtag("z", hi_ - lo_),
// xtag("hi", hi_));
::munmap(lo_, hi_ - lo_);
}
/* Mandatory hygiene: zero the bookkeeping tail so no dangling
* pointers survive (e.g. after ~DArena() runs the dtor of a
* stack-owned arena). config_, page_z_, arena_align_z_ are
* preserved -- only {lo_ .. last_error_} get cleared.
*
* The memset goes through a std::launder'd self pointer so the
* compiler cannot prove it writes to the dying object and elide
* the stores as dead (gcc>=15 does that with a plain member
* assignment / un-laundered memset at -O1). All cleared fields
* are trivially-copyable, so byte-zeroing them is well-defined.
*/
DArena * self = std::launder(this);
byte * tail = reinterpret_cast<byte *>(&self->lo_);
byte * end = reinterpret_cast<byte *>(self) + sizeof(DArena);
::memset(tail, 0, end - tail);
}
DArena::~DArena()
{
this->unmap();
}
auto
DArena::obj2hdr(void * obj) noexcept -> header_type *
{
assert(config_.store_header_flag_);
return (header_type *)((byte *)obj - sizeof(header_type));
}
auto
DArena::obj2hdr(void * obj) const noexcept -> const header_type *
{
assert(config_.store_header_flag_);
return (const header_type *)((byte *)obj - sizeof(header_type));
}
void
DArena::visit_pools(const MemorySizeVisitor & fn) const
{
/** arena can't tell purpose of allocated memory;
* must assume it's all used
**/
// assemble histogram
MemorySizeInfo::DetailArrayType detail_v;
MemorySizeInfo::DetailArrayType * p_detail = nullptr;
if (config_.store_header_flag_) {
p_detail = &detail_v;
for (const auto & ix : *this) {
typeseq ix_tseq(ix.tseq());
// totals in detail_v[0]
MemorySizeDetail & d = detail_v[0];
++d.n_alloc_;
d.z_alloc_ += ix.size();
// O(n) insertion here
for (size_t i = 1; i < detail_v.size(); ++i) {
if (detail_v[i].tseq_.is_sentinel()
|| (detail_v[i].tseq_ == ix_tseq))
{
MemorySizeDetail & d = detail_v[i];
d.tseq_ = ix_tseq;
++d.n_alloc_;
d.z_alloc_ += ix.size();
break;
}
}
}
}
fn(MemorySizeInfo(config_.name_,
this->allocated() /*used*/,
this->allocated(),
this->committed(),
this->reserved(),
lo_,
hi_,
p_detail));
}
AllocInfo
DArena::alloc_info(value_type mem) const noexcept
{
if (!config_.store_header_flag_) [[unlikely]] {
this->capture_error(error::alloc_info_disabled, __PRETTY_FUNCTION__);
return AllocInfo::error_not_configured(&config_.header_);
}
byte * header_mem = mem - sizeof(AllocHeader);
#ifdef OBSOLETE // relying on cross-alloc header shenanigans in DX1Collector
if (!this->contains(header_mem)) {
this->capture_error(error::alloc_info_address);
}
#endif
AllocHeader * header = (AllocHeader *)header_mem;
const byte * guard_lo
= header_mem - config_.header_.guard_z_;
const byte * guard_hi
= mem + config_.header_.size(*header);
return AllocInfo(&config_.header_,
guard_lo,
(AllocHeader *)header_mem,
guard_hi);
}
DArenaIterator
DArena::begin() const noexcept
{
return DArenaIterator::begin(this);
}
DArenaIterator
DArena::end() const noexcept
{
return DArenaIterator::end(this);
}
AllocHeader *
DArena::begin_header() const noexcept
{
if (config_.store_header_flag_ == false) {
this->capture_error(error::alloc_iterator_not_supported, __PRETTY_FUNCTION__);
return nullptr;
}
return (AllocHeader *)(lo_ + config_.header_.guard_z_);
}
AllocHeader *
DArena::end_header() const noexcept
{
if (config_.store_header_flag_ == false) {
this->capture_error(error::alloc_iterator_not_supported, __PRETTY_FUNCTION__);
return nullptr;
}
return (AllocHeader *)free_;
}
std::byte *
DArena::alloc(typeseq t, std::size_t req_z)
{
/* - primary allocation path:
* exactly 1 header per alloc() call.
* - store_header_flag follows configuration
*/
return _alloc(req_z,
alloc_mode::standard,
t,
0 /*age*/,
__PRETTY_FUNCTION__);
}
std::byte *
DArena::super_alloc(typeseq t, std::size_t req_z)
{
/* - (uncommon) pattern for parent alloc immediately followed by
* zero-or-more susidiary allocs, all sharing a single header.
* - collapses into alloc() behavior when
* ArenaConfig.store_header_flag_ disabled
*/
(void)t;
return _alloc(req_z,
alloc_mode::super,
t,
0 /*age*/,
__PRETTY_FUNCTION__);
}
std::byte *
DArena::sub_alloc(std::size_t req_z,
bool complete_flag)
{
/* - (uncommon) pattern for subsidiary allocs:
* that piggyback onto preceding super_alloc()
* - collapses into alloc() behavior when
* ArenaConfig.store_header_flag_ disabled
*/
return _alloc(req_z,
(complete_flag
? alloc_mode::sub_complete
: alloc_mode::sub_incomplete),
typeseq::sentinel() /*typeseq: ignored*/,
0 /*age - ignored */,
__PRETTY_FUNCTION__);
}
std::byte *
DArena::alloc_copy(std::byte * src)
{
/* NOTE: allocator that owns src must have the same header configuration */
assert(config_.store_header_flag_);
/* src will come from an allocator other than this one;
* we rely on header layout from destination
* allocator -> assumes compatible header config
*/
AllocInfo src_info = alloc_info(src);
size_t req_z = src_info.size();
typeseq tseq = typeseq(src_info.tseq());
uint32_t age = src_info.age();
return _alloc(req_z, alloc_mode::standard, tseq, age + 1,
__PRETTY_FUNCTION__);
}
void
DArena::capture_error(error err,
const char * src_fn,
size_type target_z) const
{
DArena * self = const_cast<DArena *>(this);
++(self->error_count_);
self->last_error_ = AllocError(err,
src_fn,
error_count_,
target_z,
committed_z_,
reserved());
}
byte *
DArena::_alloc(std::size_t req_z,
alloc_mode mode,
typeseq tseq,
uint32_t age,
const char * src_fn)
{
scope log(XO_DEBUG(config_.debug_flag_));
/*
* sub_complete
* sub_incomplete |
* standard super | |
* v v v v
*/
std::array<bool, 4> store_header_v = {{ true, true, false, false }};
std::array<bool, 4> retain_header_v = {{ false, true, false, false }};
std::array<bool, 4> store_guard_v = {{ true, false, false, true }};
/* -> write header at free_ */
bool store_header_flag = false;
/* -> stash last_header_*/
bool retain_header_flag = false;
/* -> write guard bytes */
bool store_guard = false;
if (config_.store_header_flag_) {
store_header_flag = store_header_v[(int)mode];
retain_header_flag = retain_header_v[(int)mode];
store_guard = store_guard_v[(int)mode];
}
assert(padding::is_aligned((size_t)free_));
/*
* free_(pre)
* v
*
* <-------------z1--------------->
* < guard >< hz >< req_z >< dz >< guard >
*
* used <== +++++++++0000zzzz@@@@@@@@@@@@@@@@@ppppppp+++++++++ ==> avail
*
* ^ ^ ^
* header mem |
* ^ |
* last_header_ free_(post)
*
* [+] guard after each allocation, for simple sanitize checks
* [0] unused header bits (avail to application)
* [z] record allocation size
* [@] new allocated memory
* [p] padding (to uintptr_t alignment)
*/
/* non-zero if header feature enabled */
size_t hz = 0;
/* dz: pad req_z to alignment size (multiple of 8 bytes, probably) */
size_t dz = padding::alloc_padding(req_z);
size_t z0 = req_z + dz;
/* if non-zero:
* will store padded alloc size at the beginning of each allocation
* reminder:
* important to store padded size for correct arena iteration
*/
uint64_t header = (req_z + dz);
if (store_header_flag)
{
if (config_.header_.is_size_enabled()) [[likely]] {
header = this->config_.header_.mkheader(tseq.seqno(), age, req_z + dz);
hz = sizeof(header);
} else {
/* req_z doesn't fit in configured header_size_mask bits */
capture_error(error::header_size_mask, src_fn);
return nullptr;
}
}
size_t z1 = hz + z0;
assert(padding::is_aligned(z1));
if (!this->expand(this->allocated() + z1, src_fn)) [[unlikely]] {
/* (error state already captured) */
return nullptr;
}
if (store_header_flag) {
/* capturing header */
*(uint64_t *)free_ = header;
if (retain_header_flag) {
/* and rembering for subsequent
* sub_alloc()
*/
last_header_ = (AllocHeader *)free_;
}
}
byte * mem = free_ + hz;
this->free_ += z1;
if (store_guard) {
/* write guard bytes for overrun detection */
::memset(free_,
config_.header_.guard_byte_,
config_.header_.guard_z_);
this->free_ += config_.header_.guard_z_;
}
log && log(xtag("self", config_.name_),
xtag("hz", hz),
xtag("z0", req_z),
xtag("+pad", dz),
xtag("z1", z1),
xtag("size", this->committed()),
xtag("avail", this->available()));
log && log(xtag("mem", mem),
xtag("free", free_));
return mem;
}
void
DArena::establish_initial_guard() noexcept
{
assert(free_ == lo_);
::memset(this->free_,
config_.header_.guard_byte_,
config_.header_.guard_z_);
this->free_ += config_.header_.guard_z_;
}
bool
DArena::expand(size_t target_z, const char * src_fn) noexcept
{
scope log(XO_DEBUG(config_.debug_flag_),
xtag("target_z", target_z),
xtag("committed_z", committed_z_),
xtag("src_fn", src_fn));
if (target_z <= committed_z_) [[likely]] {
log && log("trivial success, offset within committed range",
xtag("target_z", target_z),
xtag("committed_z", committed_z_));
return true;
}
if (lo_ + target_z > hi_) [[unlikely]] {
this->capture_error(error::reserve_exhausted, src_fn, target_z);
//fprintf(stderr, "DArena::expand: reserve exhausted");
//print_backtrace_dwarf(true /*demangle_flag*/);
//std::terminate();
return false;
}
/*
* pre:
*
* _______________...................................
* ^ ^ ^
* lo limit hi
*
* < committed_z >
* <----------target_z----------->
* > <- z: 0 <= z < hugepage_z
* <---------aligned_target_z--------->
* <--- add_commit_z -->
*
* post:
* ____________________________________..............
* ^ ^ ^
* lo limit hi
*
*/
std::size_t aligned_target_z = padding::with_padding(target_z, arena_align_z_);
std::byte * commit_start = limit_; // = lo_ + committed_z_;
std::size_t add_commit_z = aligned_target_z - committed_z_;
assert(limit_ == lo_ + committed_z_);
if (::mprotect(commit_start,
add_commit_z,
PROT_READ | PROT_WRITE) != 0) [[unlikely]]
{
if (log) {
log("commit failed!");
log(xtag("aligned_target_z", aligned_target_z),
xtag("commit_start", commit_start),
xtag("add_commit_z", add_commit_z),
xtag("commit_end", commit_start + add_commit_z)
);
}
this->capture_error(error::commit_failed, src_fn, add_commit_z);
fprintf(stderr, "DArena::expand: mprotect failed (system oom?)");
print_backtrace_dwarf(false /*!demangle_flag*/);
return false;
}
committed_z_ = aligned_target_z;
limit_ = lo_ + committed_z_;
if (commit_start == lo_) [[unlikely]] {
/* first expand() for this allocator - start with guard_z_ bytes */
this->establish_initial_guard();
}
assert(committed_z_ % arena_align_z_ == 0);
assert(reinterpret_cast<size_t>(limit_) % arena_align_z_ == 0);
return true;
} /*expand*/
void
DArena::scrub() noexcept
{
::memset(this->lo_, 0, this->free_ - this->lo_);
}
void
DArena::clear() noexcept
{
this->free_ = lo_;
this->establish_initial_guard();
}
void
DArena::swap(DArena & other) noexcept
{
std::swap(config_, other.config_);
std::swap(page_z_, other.page_z_);
std::swap(arena_align_z_, other.arena_align_z_);
std::swap(lo_, other.lo_);
std::swap(committed_z_, other.committed_z_);
std::swap(last_header_, other.last_header_);
std::swap(free_, other.free_);
std::swap(limit_, other.limit_);
std::swap(hi_, other.hi_);
std::swap(error_count_, other.error_count_);
std::swap(last_error_, other.last_error_);
}
}
} /*namespace xo*/
/* end DArena.cpp */

View file

@ -1,144 +0,0 @@
/** @file DArenaIterator.cpp
*
* @author Roland Conybeare, Dec 2025
**/
#include "DArenaIterator.hpp"
#include "DArena.hpp"
#include <xo/indentlog/scope.hpp>
#include <xo/indentlog/print/tag.hpp>
#include <cassert>
namespace xo {
using std::byte;
namespace mm {
DArenaIterator
DArenaIterator::begin(const DArena * arena)
{
constexpr bool c_debug_flag = false;
scope log(XO_DEBUG(c_debug_flag));
AllocHeader * begin_hdr = begin_header(arena);
if (!begin_hdr)
return DArenaIterator::invalid();
log && log(xtag("begin_hdr", begin_hdr));
return DArenaIterator(arena, begin_hdr);
}
DArenaIterator
DArenaIterator::end(const DArena * arena)
{
constexpr bool c_debug_flag = false;
scope log(XO_DEBUG(c_debug_flag));
AllocHeader * end_hdr = end_header(arena);
if (!end_hdr)
return DArenaIterator::invalid();
log && log(xtag("end_hdr", end_hdr));
return DArenaIterator(arena, end_hdr);
}
AllocHeader *
DArenaIterator::begin_header(const DArena * arena)
{
assert(arena);
return arena->begin_header();
}
AllocHeader *
DArenaIterator::end_header(const DArena * arena)
{
assert(arena);
return arena->end_header();
}
AllocInfo
DArenaIterator::deref() const noexcept
{
constexpr bool c_debug_flag = false;
scope log(XO_DEBUG(c_debug_flag));
bool contains_flag = arena_->contains(this->pos_as_byte());
bool bounds_flag = (this->pos_as_byte() < arena_->free_);
log && log(xtag("contains_flag", contains_flag),
xtag("bounds_flag", bounds_flag));
if (!contains_flag || !bounds_flag) {
arena_->capture_error(error::alloc_iterator_deref, __PRETTY_FUNCTION__);
return AllocInfo::error_invalid_iterator(&(arena_->config_.header_));
}
/* iterator points to beginning of header.
* memory given to application start immediately followed header
*/
byte * mem = (byte *)(pos_ + 1);
return arena_->alloc_info(mem);
}
cmpresult
DArenaIterator::compare(const DArenaIterator & other_ix) const noexcept
{
scope log(XO_DEBUG(false),
xtag("arena", arena_),
xtag("pos", pos_),
xtag("other.arena", other_ix.arena_),
xtag("other.pos", other_ix.pos_));
if (is_invalid() || (arena_ != other_ix.arena_))
return cmpresult::incomparable();
if (pos_ < other_ix.pos_) {
return cmpresult::lesser();
} else if(pos_ == other_ix.pos_) {
return cmpresult::equal();
} else {
return cmpresult::greater();
}
}
void
DArenaIterator::next() noexcept
{
constexpr bool c_debug_flag = false;
scope log(XO_DEBUG(c_debug_flag));
bool contains_flag = arena_->contains(this->pos_as_byte());
bool bounds_flag = (this->pos_as_byte() < arena_->free_);
log && log(xtag("contains_flag", contains_flag),
xtag("bounds_flag", bounds_flag));
if (!contains_flag || !bounds_flag) {
arena_->capture_error(error::alloc_iterator_next, __PRETTY_FUNCTION__);
return;
}
size_t mem_z = arena_->config_.header_.size(*pos_);
size_t guard_z = arena_->config_.header_.guard_z_;
byte * next_as_byte = ((byte *)pos_ + sizeof(AllocHeader) + mem_z + guard_z);
/* next == ix.arena_free_ --> iterator is at end of allocator */
assert(next_as_byte <= arena_->free_);
AllocHeader * next = (AllocHeader *)next_as_byte;
this->pos_ = next;
}
} /*namespace mm*/
} /*namespace xo*/
/* end DArenaIterator.cpp */

View file

@ -1,394 +0,0 @@
/** @file DCircularBuffer.cpp
*
* @author Roland Conybeare, Jan 2026
**/
#include "DCircularBuffer.hpp"
#include "mmap_util.hpp"
#include <xo/indentlog/scope.hpp>
#include <xo/indentlog/print/tostr.hpp>
#include <sys/mman.h>
#include <unistd.h> // for ::getpagesize() on osx
namespace xo {
using xo::print::operator<<;
using xo::print::printspan;
namespace mm {
DCircularBuffer::DCircularBuffer(DCircularBuffer && other)
: config_{other.config_},
page_z_{other.page_z_},
buffer_align_z_{other.buffer_align_z_},
reserved_range_{other.reserved_range_},
mapped_range_{other.mapped_range_},
occupied_range_{other.occupied_range_},
pinned_spans_{std::move(other.pinned_spans_)}
{
other.reserved_range_ = span_type();
other.mapped_range_ = span_type();
other.occupied_range_ = span_type();
}
DCircularBuffer
DCircularBuffer::map(const CircularBufferConfig & config)
{
scope log(XO_DEBUG(config.debug_flag_));
/* vm page size. 4KB (probably if linux) or 16KB (probably if osx) */
size_t page_z = getpagesize();
bool enable_hugepage_flag = (config.max_capacity_ >= config.hugepage_z_);
/* Align start of arena memory on this boundary.
* Will use THP (transparent huge pages) if available
* and arena size is at least as large as hugepage size (2MB, probably)
*/
size_t align_z = (enable_hugepage_flag ? config.hugepage_z_ : page_z);
log && log(xtag("page_z", page_z),
xtag("align_z", align_z));
auto mapped_span
= span<char>::from_memory(mmap_util::map_aligned_range
(config.max_capacity_,
align_z,
enable_hugepage_flag,
config.debug_flag_));
if (!mapped_span.lo()) {
throw std::runtime_error(tostr("DCircularBuffer: reserve address range failed",
xtag("size", config.max_capacity_)));
}
return DCircularBuffer(config, page_z, align_z, mapped_span);
}
DCircularBuffer::DCircularBuffer(const CircularBufferConfig & config,
size_type page_z,
size_type buffer_align_z,
span_type reserved_range)
: config_{config},
page_z_{page_z},
buffer_align_z_{buffer_align_z},
reserved_range_{reserved_range},
mapped_range_{reserved_range_.prefix(0)},
occupied_range_{mapped_range_.prefix(0)},
input_range_{occupied_range_.prefix(0)},
pinned_spans_{DArenaVector<span_type>::map(ArenaConfig().with_name(config.name_ + "-pins"))}
{
}
void
DCircularBuffer::visit_pools(const MemorySizeVisitor & visitor) const
{
visitor(MemorySizeInfo(config_.name_,
occupied_range_.size() /*used*/,
occupied_range_.size(),
mapped_range_.size(),
reserved_range_.size(),
reserved_range_.lo(),
reserved_range_.hi(),
nullptr /*detail*/));
pinned_spans_.visit_pools(visitor);
}
bool
DCircularBuffer::verify_ok(verify_policy policy) const
{
using xo::xtag;
constexpr const char * c_self = "DCircularBuffer::verify_ok";
scope log(XO_DEBUG(false));
/* CB1: mapped_range_ is subrange of reserved_range_ */
if ((mapped_range_.lo() < reserved_range_.lo())
|| (mapped_range_.hi() > reserved_range_.hi()))
{
return policy.report_error(log,
c_self, ": expect mapped_range subset of reserved_range",
xtag("mapped.lo", (void*)mapped_range_.lo()),
xtag("mapped.hi", (void*)mapped_range_.hi()),
xtag("reserved.lo", (void*)reserved_range_.lo()),
xtag("reserved.hi", (void*)reserved_range_.hi()));
}
/* CB2: occupied_range_ is subrange of mapped_range_ */
if ((occupied_range_.lo() < mapped_range_.lo())
|| (occupied_range_.hi() > mapped_range_.hi()))
{
return policy.report_error(log,
c_self, ": expect occupied_range subset of mapped_range",
xtag("occupied.lo", (void*)occupied_range_.lo()),
xtag("occupied.hi", (void*)occupied_range_.hi()),
xtag("mapped.lo", (void*)mapped_range_.lo()),
xtag("mapped.hi", (void*)mapped_range_.hi()));
}
/* CB3: each remembered span is subrange of occupied_range_ */
for (size_type i = 0, n = pinned_spans_.size(); i < n; ++i) {
const const_span_type & pin = pinned_spans_[i];
if ((pin.lo() < occupied_range_.lo())
|| (pin.hi() > occupied_range_.hi()))
{
return policy.report_error(log,
c_self, ": expect remembered_span subset of occupied_range",
xtag("i", i),
xtag("pin.lo", (void*)pin.lo()),
xtag("pin.hi", (void*)pin.hi()),
xtag("occupied.lo", (void*)occupied_range_.lo()),
xtag("occupied.hi", (void*)occupied_range_.hi()));
}
}
/* CB4: buffer_align_z_ is non-zero (when buffer is mapped) */
if (!reserved_range_.is_null() && (buffer_align_z_ == 0)) {
return policy.report_error(log,
c_self, ": expect buffer_align_z > 0 when buffer is mapped",
xtag("buffer_align_z", buffer_align_z_));
}
/* CB5: reserved_range_ aligned on buffer_align_z_ boundary */
if (!reserved_range_.is_null() && (buffer_align_z_ > 0)) {
if (((size_type)(reserved_range_.lo()) % buffer_align_z_) != 0) {
return policy.report_error(log,
c_self, ": expect reserved_range.lo aligned on buffer_align_z",
xtag("reserved.lo", (void*)reserved_range_.lo()),
xtag("buffer_align_z", buffer_align_z_));
}
}
return true;
}
auto
DCircularBuffer::append(const_span_type src) -> const_span_type
{
span_type dest = get_append_span(src.size());
size_t copy_z = std::min(src.size(), dest.size());
::memcpy(occupied_range_.hi(), src.lo(), copy_z);
this->occupied_range_ += span_type(dest.lo(), copy_z);
this->input_range_ += span_type(dest.lo(), copy_z);
return src.after_prefix(copy_z);
}
auto
DCircularBuffer::get_append_span(size_type desired_z) -> span_type
{
span_type dest = span_type(occupied_range_.hi(), desired_z);
if (dest.hi() > reserved_range_.hi()) {
/* under no circumstances go past the end of reserved range */
dest = span_type(dest.lo(), reserved_range_.hi());
}
/* establish mapped range at least to dest.hi */
this->_expand_to(dest.hi());
/* report available memory */
return span_type(occupied_range_.hi(), mapped_range_.hi());
}
void
DCircularBuffer::report_append(span_type r)
{
if (r.lo() != occupied_range_.hi()) {
// error!
// this->capture_error(error::bad_append_report, r.size())
assert(false);
return;
}
if (r.hi() > mapped_range_.hi()) {
// error!
// this->capture_error(error::bad_append_report, r.size())
assert(false);
return;
}
this->occupied_range_ += r;
}
void
DCircularBuffer::consume(const_span_type input)
{
scope log(XO_DEBUG(false), xtag("input", input.to_string_view()));
if (input.lo() != input_range_.lo()) {
assert(false);
return;
}
if (input.hi() > occupied_range_.hi()) {
assert(false);
return;
}
if (occupied_range_.lo() < input_range_.lo()) {
log && log("pinned range prevents shrinking occupied range");
/* here: a pinned range prevents shrinking occupied_range */
this->input_range_
= input_range_.suffix_from((span_type::value_type *)input.hi());
} else {
log && log(xtag("msg", "will shrink occupied range"),
xtag("input.lo", (void*)input.lo()),
xtag("input.hi", (void*)input.hi()),
xtag("stored.lo", (void*)input_range_.lo()),
xtag("stored.hi", (void*)input_range_.hi())
);
/* here: input; recompute occupied boundary */
this->input_range_
= input_range_.suffix_from((span_type::value_type *)input.hi());
log && log(xtag("occupied", occupied_range_.size()),
xtag("input", input_range_.size()));
this->_shrink_occupied_to_fit();
log && log(xtag("occupied", occupied_range_.size()),
xtag("input", input_range_.size()));
}
this->_check_reset_map_start();
}
void
DCircularBuffer::pin_range(span_type r)
{
// loop optimized for case where r falls
// _after_ any existing pinned ranges
size_type z = pinned_spans_.size();
size_type ip1 = z; // ip1 = i + 1
for (; ip1 > 0; --ip1) {
if (r.lo() > pinned_spans_[ip1 - 1].lo())
break;
// insert at i to maintain sorted order
pinned_spans_.insert(ip1 - 1, r);
return;
}
pinned_spans_.push_back(r);
}
void
DCircularBuffer::unpin_range(span_type r)
{
// loop optimized for case where r
// is the first pinned range
assert(pinned_spans_.size() > 0);
if (r == pinned_spans_[0]) {
this->pinned_spans_.erase(0);
/* removing pinned span means can perhaps shrink
* occupied range
*/
this->_shrink_occupied_to_fit();
this->_check_reset_map_start();
} else {
for (size_type i = 1; i < pinned_spans_.size(); ++i) {
if (r == pinned_spans_[i]) {
this->pinned_spans_.erase(i);
/* since this isn't the first pinned span,
* won't be able to shrink occupied range.
*/
return;
}
}
}
}
bool
DCircularBuffer::_expand_to(char * hi)
{
scope log(XO_DEBUG(config_.debug_flag_));
if (hi < mapped_range_.hi()) {
/* nothing todo */
return true;
}
size_t add_z = hi - mapped_range_.hi();
size_t add_commit_z = padding::with_padding(add_z, buffer_align_z_);
char * commit_start = mapped_range_.hi();
if (::mprotect(commit_start,
add_commit_z,
PROT_READ | PROT_WRITE) != 0)
{
if (log) {
log("commit failed");
log(xtag("commit_start", commit_start),
xtag("add_z", add_z),
xtag("add_commit_z", add_commit_z));
}
// this->capture_error(error::commit_failed, add_commit_z);
return false;
}
this->mapped_range_ += span(commit_start, add_commit_z);
return true;
}
void
DCircularBuffer::_shrink_occupied_to_fit()
{
if (pinned_spans_.empty()) {
this->occupied_range_ = input_range_;
} else if (occupied_range_.lo() < pinned_spans_[0].lo()) {
this->occupied_range_ = occupied_range_.suffix_from(pinned_spans_[0].lo());
}
}
void
DCircularBuffer::_check_reset_map_start()
{
if (pinned_spans_.empty()
&& (input_range_ == occupied_range_)) {
// here: permissible to move input range to the beginning of mapped range.
// decide (heuristically) whether we think this is optimal
std::size_t input_z = input_range_.size();
// 1st clause checks efficiency.
// 2nd clause (probably redundant) check non-overlapping
if ((input_range_.lo() > (mapped_range_.lo()
+ std::max(page_z_,
static_cast<size_type>(config_.threshold_move_efficiency_ * input_z))))
&& (mapped_range_.lo() + input_z < input_range_.lo())) {
::memmove(mapped_range_.lo(), input_range_.lo(), input_z);
this->occupied_range_ = mapped_range_.prefix(input_z);
this->input_range_ = mapped_range_.prefix(input_z);
}
}
}
} /*namespace mm*/
} /*namespace xo*/
/* end DCircularBuffer.cpp */

View file

@ -1,43 +0,0 @@
/** @file ErrorArena.cpp
*
* @author Roland Conybeare, Feb 2026
**/
#include "ErrorArena.hpp"
namespace xo {
namespace mm {
DArena
ErrorArena::s_instance;
ArenaConfig
ErrorArena::default_config()
{
return ArenaConfig().with_name("error-arena").with_size(16 * 1024);
}
namespace {
bool s_init_done = false;
}
void
ErrorArena::init_once(const ArenaConfig & cfg)
{
if (!s_init_done) {
s_init_done = true;
s_instance = DArena::map(cfg);
}
}
DArena *
ErrorArena::instance()
{
init_once(default_config());
return &s_instance;
}
} /*namespace mm*/
} /*namespace xo*/
/* end ErrorArena.cpp */

View file

@ -1,214 +0,0 @@
/** @file arena_streambuf.cpp
*
* @author Roland Conybeare, Feb 2026
**/
#include "arena_streambuf.hpp"
namespace xo {
namespace mm {
std::uint32_t
arena_streambuf::lpos() const
{
if (debug_flag_) {
std::cerr << "log_streambuf::lpos: enter" << std::endl;
}
// logically-const. lazy implementation
arena_streambuf * self = const_cast<arena_streambuf *>(this);
self->_check_update_local_state();
return pos() - solpos_ - color_escape_chars_;
}
auto
arena_streambuf::checkpoint() const -> rewind_state
{
// logically-const. lazy implementation
arena_streambuf * self = const_cast<arena_streambuf *>(this);
self->_check_update_local_state();
return rewind_state(solpos_, color_escape_chars_, pos());
}
void
arena_streambuf::reset_stream()
{
assert(arena_);
assert(arena_->committed() > 0);
char * p_lo = (char *)(arena_->lo_);
char * p_hi = (char *)(arena_->limit_);
/* tells parent our buffer extent */
this->setp(p_lo, p_hi);
this->local_ppos_ = 0;
this->solpos_ = 0;
this->color_escape_chars_ = 0;
this->color_escape_start_ = nullptr;
}
void
arena_streambuf::rewind_to(rewind_state s)
{
if (debug_flag_) {
std::cout << "rewind_to: pos " << pos() << "->" << s.pos
<< " solpos " << solpos_ << "->" << s.solpos
<< " color_esc " << color_escape_chars_ << "->" << s.color_escape_chars
<< std::endl;
}
/* .setp(): using just for side effect: sets .pptr to .pbase */
this->setp(this->pbase(), this->epptr());
/* advance pptr to saved position */
this->pbump(s.pos);
this->local_ppos_ = this->pptr() - this->pbase();
this->solpos_ = s.solpos;
this->color_escape_chars_ = s.color_escape_chars;
/* assuming we never try to capture rewind state with incomplete color escape */
this->color_escape_start_ = nullptr;
}
void
arena_streambuf::expand_to(std::size_t new_z)
{
char * old_pptr = pptr();
std::streamsize old_n = old_pptr - pbase();
assert(old_n <= static_cast<std::streamsize>(arena_->allocated()));
assert(new_z > arena_->committed());
/* note: local_ppos_ invariant across expand_to() */
arena_->expand(new_z, __PRETTY_FUNCTION__);
char * p_base = (char *)(arena_->lo_);
char * p_hi = (char *)(arena_->limit_);
this->setp(p_base, p_hi);
this->pbump(old_n);
}
std::streamsize
arena_streambuf::xsputn(const char * s, std::streamsize n)
{
/* s must be an address in [this->lo() .. this->lo() + capacity()] */
assert(hi() >= pptr());
if (pptr() + n > hi()) {
std::size_t new_z = std::max(2 * arena_->committed(), std::size_t(this->pos() + n + 1));
if (new_z > arena_->reserved())
new_z = arena_->reserved();
this->expand_to(new_z);
}
if (debug_flag_) {
std::cout << "xsputn: pbase=" << (void *)(this->pbase())
<< ", pptr=" << (void*)(this->pptr())
<< "(+" << (this->pptr() - this->lo()) << ")"
<< ", n=" << n << " -> (+" << (this->pptr() + n - this->lo()) << ")"
<< ", arena.size=" << this->arena_->committed()
<< std::endl;
}
std::streamsize ncopied = 0;
if (this->pptr() + n > this->hi()) {
ncopied = this->hi() - this->pptr();
} else {
ncopied = n;
}
if (false /*debug_flag_*/) {
std::cout << "xsputn: copying ncopied=" << ncopied << " (/n=" << n << ") bytes into range [lo,hi)"
<< ", lo=" << (void*)this->pptr()
<< ", hi=" << (void*)(this->pptr() + n)
<< std::endl;
}
std::memcpy(this->pptr(), s, ncopied);
this->pbump(ncopied);
/* now {pbase, pptr} consistent with new input */
this->_check_update_local_state();
return ncopied;
}
auto
arena_streambuf::overflow(int_type new_ch) -> int_type
{
char * old_base = this->pbase();
char * old_pptr = this->pptr();
/* #of chars buffered */
std::streamsize old_n = old_pptr - old_base;
assert(old_n <= static_cast<std::streamsize>(arena_->committed()));
// if (debug_flag_) {
// std::cout << "overflow: new_ch=" << quoted_char(new_ch) << std::endl;
// }
/* increase buffer size */
this->expand_to(2 * arena_->committed());
arena_->lo_[old_n] = static_cast<std::byte>(new_ch);
this->pbump(1);
if ((new_ch == static_cast<int_type>('\n')) || (new_ch == static_cast<int_type>('\r'))) {
this->solpos_ = this->pos();
// what if new_ch starts color escape ?
}
if (new_ch == std::char_traits<char>::eof()) {
/* reminder: returning eof sets badbit on ostream */
return std::char_traits<char>::not_eof(new_ch);
} else {
return new_ch;
}
}
auto
arena_streambuf::seekoff(off_type off,
std::ios_base::seekdir dir,
std::ios_base::openmode which) -> pos_type
{
//std::cout << "seekoff: off=" << off << ", dir=" << dir << ", which=" << which << std::endl;
if (debug_flag_) {
std::cout << "seekoff(off,dir,which)" << std::endl;
}
// Only output stream is supported
if (which != std::ios_base::out)
throw std::runtime_error("log_streambuf: only output mode supported");
if (dir == std::ios_base::cur) {
this->pbump(off);
} else if (dir == std::ios_base::end) {
/* .setp(): using for side effect: sets .pptr to .pbase */
this->setp(this->pbase(), this->epptr());
this->pbump(off);
} else if (dir == std::ios_base::beg) {
/* .setp(): using for side effect: sets .pptr to .pbase */
this->setp(this->pbase(), this->epptr());
this->pbump(this->capacity() + off);
}
return this->pptr() - this->pbase();
} /*seekoff*/
} /*namespace mm*/
} /*namespace xo*/
/* end arena_streambuf.cpp */

View file

@ -1,172 +0,0 @@
/** @file backtrace.cpp
*
* @author Roland Conybeare, Apr 2026
**/
#include "backtrace.hpp"
#include <iostream>
#include <array>
#include <cstdio>
#include <cstdlib>
#include <libunwind.h>
#include <cxxabi.h>
#include <unistd.h>
#ifndef __APPLE__
# include <elfutils/libdwfl.h>
#endif
namespace xo {
void
print_backtrace(bool demangle_flag) {
unw_cursor_t cursor;
unw_context_t cx;
// capture cpu register state at this call site
unw_getcontext(&cx);
// stack frame iterator for current thread.
// local -> this process
//
unw_init_local(&cursor, &cx);
// depth relative to top of call stack
int depth = 0;
while (unw_step(&cursor) > 0) {
unw_word_t pc = 0;
// read return address of current frame into pc.
// This determines the function that is executing
// when print_backtrace() invoked
//
unw_get_reg(&cursor, UNW_REG_IP, &pc);
std::array<char, 256> name;
unw_word_t offset = 0;
// mangled function name for current frame's pc.
//
if (unw_get_proc_name(&cursor, name.data(), sizeof(name), &offset) == 0) {
int status = 0;
// we are resaponsible for calling ::free() on non-null demangled value
char * demangled = nullptr;
if (demangle_flag)
demangled = abi::__cxa_demangle(name.data(), nullptr, nullptr, &status);
if ((status == 0) && demangled) {
fprintf(stderr, "#%d 0x%lx %s+0x%lx\n",
depth, (long)pc, demangled, (long)offset);
free(demangled);
} else {
// demangle failed (or disabled)
fprintf(stderr, "#%d 0x%lx %s+0x%lx\n",
depth, (long)pc, name.data(), (long)offset);
}
} else {
// unable to get function name
fprintf(stderr, "#%d 0x%lx ???\n", depth, (long)pc);
}
}
}
namespace {
#ifndef __APPLE__
// libdwfl requires callbacks for find_elf and find_debuginfo.
// The offline defaults work for the current process.
//
static const Dwfl_Callbacks dwfl_callbacks = {
.find_elf = dwfl_linux_proc_find_elf,
.find_debuginfo = dwfl_standard_find_debuginfo,
.section_address = nullptr,
.debuginfo_path = nullptr,
};
#endif
}
void
print_backtrace_dwarf(bool demangle_flag)
{
#ifdef __APPLE__
(void)demangle_flag;
std::cerr << "backtrace with dwarf symbols not supported on osx" << std::endl;
#else
unw_cursor_t cursor;
unw_context_t cx;
unw_getcontext(&cx);
unw_init_local(&cursor, &cx);
// set up dwfl for resolving addresses to source locations.
//
Dwfl * dwfl = dwfl_begin(&dwfl_callbacks);
if (dwfl) {
// populate module list from /proc/self/maps
dwfl_linux_proc_report(dwfl, getpid());
dwfl_report_end(dwfl, nullptr, nullptr);
}
int depth = 0;
while (unw_step(&cursor) > 0) {
unw_word_t pc = 0;
unw_get_reg(&cursor, UNW_REG_IP, &pc);
std::array<char, 256> name;
unw_word_t offset = 0;
// resolve function name via libunwind
//
const char * func_name = "???";
char * demangled = nullptr;
int status = -1;
if (unw_get_proc_name(&cursor, name.data(), name.size(), &offset) == 0) {
if (demangle_flag)
demangled = abi::__cxa_demangle(name.data(), nullptr, nullptr, &status);
func_name = ((status == 0) && demangled) ? demangled : name.data();
}
// resolve source file + line via DWARF debug info
//
const char * source_file = nullptr;
int line = 0;
if (dwfl) {
Dwfl_Module * module = dwfl_addrmodule(dwfl, pc);
if (module) {
Dwfl_Line * dwfl_line = dwfl_module_getsrc(module, pc);
if (dwfl_line) {
source_file = dwfl_lineinfo(dwfl_line, nullptr, &line,
nullptr, nullptr, nullptr);
}
}
}
if (source_file) {
fprintf(stderr, "#%d 0x%lx %s+0x%lx at %s:%d\n",
depth, (long)pc, func_name, (long)offset,
source_file, line);
} else {
fprintf(stderr, "#%d 0x%lx %s+0x%lx\n",
depth, (long)pc, func_name, (long)offset);
}
if (demangled)
free(demangled);
++depth;
}
if (dwfl)
dwfl_end(dwfl);
#endif
}
} /*namespace xo*/
/* end backtrace.cpp */

View file

@ -1,38 +0,0 @@
/** @file cmpresult.cpp
*
* @author Roland Conybeare, Dec 2025
**/
#include "cmpresult.hpp"
#include <xo/indentlog/print/tag.hpp>
#include <iostream>
namespace xo {
namespace mm {
const char *
comparison2str(comparison x)
{
switch (x) {
case comparison::invalid:
break;
case comparison::comparable:
return "cmp";
case comparison::incomparable:
return "!cmp";
}
return "?comparison";
}
void
cmpresult::display(std::ostream & os) const
{
os << "<cmpresult "
<< xtag("err", err_)
<< xtag("cmp", cmp_)
<< ">";
}
} /*namespace mm*/
} /*namespace xo*/
/* end cmpresult.cpp */

View file

@ -1,105 +0,0 @@
/** @file mmap_util.cpp
*
* @author Roland Conybeare, Jan 2026
**/
#include "mmap_util.hpp"
#include "padding.hpp"
#include <sys/mman.h> // for mmap
namespace xo {
namespace mm {
auto
mmap_util::map_aligned_range(size_t req_z,
size_t align_z,
bool enable_hugepage_flag,
bool debug_flag) -> span_type
{
scope log(XO_DEBUG(debug_flag),
xtag("req_z", req_z),
xtag("align_z", align_z));
// 1. round up to multiple of align_z
size_t target_z = padding::with_padding(req_z, align_z); // 4.
// 2. mmap() will give us page-aligned memory,
// but not hugepage-aligned.
//
// Over-request by align_z to ensure
// aligned subrange of size target_z
//
byte * base = (byte *)(::mmap(nullptr,
target_z + align_z,
PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0));
// on mmap success: upper limit of mapped address range
byte * hi = base + (target_z + align_z);
// lowest hugepage-aligned address in [base, hi)
byte * aligned_base = (byte *)(padding::with_padding((size_t)base, align_z));
// end of hugeppage-aligned range starting at aligned_base
byte * aligned_hi = aligned_base + target_z;
log && log("acquired memory [lo,hi) using mmap",
xtag("lo", base),
xtag("aligned_lo", aligned_base),
xtag("req_z", req_z),
xtag("target_z", target_z),
xtag("aligned_hi", aligned_hi),
xtag("hi", hi));
// 3. assess mmap success
{
if (base == MAP_FAILED) {
throw std::runtime_error(tostr("ArenaAlloc: uncommitted allocation failed",
xtag("size", req_z)));
}
assert((size_t)aligned_base % align_z == 0);
assert(aligned_base >= base);
assert(aligned_base < base + align_z);
}
// 4. release unaligned prefix
if (base < aligned_base) {
size_t ua_prefix = aligned_base - base;
::munmap(base, ua_prefix);
}
// 5. release unaligned suffix
if (aligned_hi < hi) {
size_t suffix = hi - aligned_hi;
::munmap(aligned_hi, suffix);
}
if (enable_hugepage_flag) {
#ifdef __linux__
/** linux:
* opt-in to transparent huge pages (THP)
* provided OS configured to support them.
* otherwise fallback gracefully.
*
* Huge pages -> use fewer TLB entries + faster
* shorter path through page table.
*
* When we commit (i.e. obtain physical memory on page fault),
* typically expect to pay ~1us per superpage.
* Much better than ~500us to commit 512 4k VM pages.
*
* But wasted if we don't use the memory.
*
* Page table has a handful of levels
**/
::madvise(aligned_base, target_z, MADV_HUGEPAGE); // 8.
#endif
}
return span_type(aligned_base, aligned_hi);
}
} /*namespace mm*/
} /*namespace xo*/
/* end mmap_util.cpp */

View file

@ -1,24 +0,0 @@
# xo-alloc2/utest/CMakeLists.txt
#
set(UTEST_EXE utest.arena)
set(UTEST_SRCS
arena_utest_main.cpp
# objectmodel.test.cpp
DArena.test.cpp
DArenaVector.test.cpp
DArenaHashMap.test.cpp
DCircularBuffer.test.cpp
# DArenaIterator.test.cpp
# random_allocs.cpp
)
if (ENABLE_TESTING)
xo_add_utest_executable(${UTEST_EXE} ${UTEST_SRCS})
xo_self_dependency(${UTEST_EXE} xo_arena)
xo_headeronly_dependency(${UTEST_EXE} randomgen)
# xo_headeronly_dependency(${UTEST_EXE} indentlog)
xo_external_target_dependency(${UTEST_EXE} Catch2 Catch2::Catch2)
endif()
# end CMakeLists.txt

View file

@ -1,219 +0,0 @@
/** @file DArena.test.cpp
*
* @author Roland Conybeare, Jan 2026
**/
#include "DArena.hpp"
#include "print.hpp"
#include <xo/indentlog/print/tag.hpp>
#include <catch2/catch.hpp>
namespace xo {
using xo::mm::DArena;
using xo::mm::AllocHeader;
using xo::mm::AllocHeaderConfig;
using xo::mm::ArenaConfig;
using xo::mm::padding;
using xo::mm::error;
using xo::reflect::typeseq;
using xo::xtag;
using std::byte;
namespace ut {
TEST_CASE("DArena-tiny", "[arena][DArena]")
{
ArenaConfig cfg { .name_ = "testarena",
.size_ = 1 };
DArena arena = DArena::map(cfg);
REQUIRE(arena.config_.name_ == cfg.name_);
REQUIRE(arena.lo_ != nullptr);
REQUIRE(arena.free_ == arena.lo_);
REQUIRE(arena.limit_ == arena.lo_);
REQUIRE(arena.hi_ != nullptr);
REQUIRE(arena.hi_ > arena.lo_);
REQUIRE(((size_t)arena.hi_ - (size_t)arena.lo_) % arena.page_z_ == 0);
REQUIRE(arena.lo_ + cfg.size_ <= arena.hi_);
/* verify arena.lo_ is aligned on a page boundary */
REQUIRE(((size_t)(arena.lo_) & (arena.page_z_ - 1)) == 0);
/* verify arena.hi_ is aligned on a hugepage boundary */
REQUIRE(((size_t)(arena.hi_) & (arena.page_z_ - 1)) == 0);
byte * lo = arena.lo_;
byte * free = arena.free_;
byte * limit = arena.limit_;
byte * hi = arena.hi_;
size_t committed_z = arena.committed_z_;
DArena arena2 = std::move(arena);
REQUIRE(arena.lo_ == nullptr);
REQUIRE(arena.free_ == nullptr);
REQUIRE(arena.limit_ == nullptr);
REQUIRE(arena.hi_ == nullptr);
REQUIRE(arena.committed_z_ == 0);
REQUIRE(arena.lo_ == nullptr);
REQUIRE(arena2.lo_ == lo);
REQUIRE(arena2.free_ == free);
REQUIRE(arena2.limit_ == limit);
REQUIRE(arena2.hi_ == hi);
REQUIRE(arena2.committed_z_ == committed_z);
}
TEST_CASE("DArena-medium", "[arena][DArena]")
{
ArenaConfig cfg { .name_ = "testarena",
.size_ = 10*1024*1024 };
DArena arena = DArena::map(cfg);
REQUIRE(arena.config_.name_ == cfg.name_);
REQUIRE(arena.lo_ != nullptr);
REQUIRE(arena.free_ == arena.lo_);
REQUIRE(arena.limit_ == arena.lo_);
REQUIRE(arena.hi_ != nullptr);
REQUIRE(arena.hi_ > arena.lo_);
REQUIRE(((size_t)arena.hi_ - (size_t)arena.lo_) % cfg.hugepage_z_ == 0);
REQUIRE(arena.lo_ + cfg.size_ <= arena.hi_);
/* verify arena.lo_ is aligned on a page boundary */
REQUIRE(((size_t)(arena.lo_) & (cfg.hugepage_z_ - 1)) == 0);
/* verify arena.hi_ is aligned on a hugepage boundary */
REQUIRE(((size_t)(arena.hi_) & (cfg.hugepage_z_ - 1)) == 0);
byte * lo = arena.lo_;
byte * free = arena.free_;
byte * limit = arena.limit_;
byte * hi = arena.hi_;
size_t committed_z = arena.committed_z_;
DArena arena2 = std::move(arena);
REQUIRE(arena.lo_ == nullptr);
REQUIRE(arena.free_ == nullptr);
REQUIRE(arena.limit_ == nullptr);
REQUIRE(arena.hi_ == nullptr);
REQUIRE(arena.committed_z_ == 0);
REQUIRE(arena.lo_ == nullptr);
REQUIRE(arena2.lo_ == lo);
REQUIRE(arena2.free_ == free);
REQUIRE(arena2.limit_ == limit);
REQUIRE(arena2.hi_ == hi);
REQUIRE(arena2.committed_z_ == committed_z);
}
TEST_CASE("DArena-expand-1", "[arena][DArena]")
{
/* typed allocator a1o */
ArenaConfig cfg { .name_ = "testarena",
.size_ = 1,
.debug_flag_ = false };
DArena arena = DArena::map(cfg);
REQUIRE(arena.available() == 0);
REQUIRE(arena.allocated() == 0);
size_t z2 = 512;
bool ok = arena.expand(z2, __PRETTY_FUNCTION__);
INFO(xtag("last_error", arena.last_error()));
REQUIRE(ok);
REQUIRE(arena.reserved() % arena.page_z() == 0);
REQUIRE(arena.committed() >= z2);
REQUIRE(arena.committed() % arena.page_z() == 0);
REQUIRE(arena.available() >= z2);
REQUIRE(arena.available() == arena.committed());
REQUIRE(arena.allocated() == 0);
}
TEST_CASE("arena-alloc-1", "[arena][DArena]")
{
/* typed allocator a1o */
ArenaConfig cfg { .name_ = "testarena",
.size_ = 64*1024,
.debug_flag_ = false };
DArena arena = DArena::map(cfg);
REQUIRE(arena.reserved() >= cfg.size_);
REQUIRE(arena.committed() == 0);
REQUIRE(arena.available() == 0);
REQUIRE(arena.allocated() == 0);
size_t z0 = 1;
byte * m0 = arena.alloc(typeseq::sentinel(), 1);
REQUIRE(m0);
REQUIRE(arena.last_error().error_ == error::ok);
REQUIRE(arena.last_error().error_seq_ == 0);
REQUIRE(arena.allocated() >= z0);
REQUIRE(arena.allocated() < z0 + padding::c_alloc_alignment );
REQUIRE(arena.allocated() <= arena.committed());
REQUIRE(arena.allocated() + arena.available() == arena.committed());
REQUIRE(arena.committed() <= arena.reserved());
size_t z1 = 16;
byte * m1 = arena.alloc(typeseq::sentinel(), z1);
REQUIRE(m1);
REQUIRE(arena.last_error().error_ == error::ok);
REQUIRE(arena.last_error().error_seq_ == 0);
REQUIRE(arena.allocated() >= z0 + z1);
REQUIRE(arena.allocated() < z0 + z1 + 2 * padding::c_alloc_alignment );
REQUIRE(arena.allocated() <= arena.committed());
REQUIRE(arena.allocated() + arena.available() == arena.committed());
REQUIRE(arena.committed() <= arena.reserved());
}
TEST_CASE("arena-alloc-2", "[arena][DArena]")
{
using header_type = AllocHeader;
/* typed allocator a1o, with object header */
ArenaConfig cfg { .name_ = "testarena",
.size_ = 64*1024,
.store_header_flag_ = true,
/* up to 4GB */
.header_ = AllocHeaderConfig(0 /*guard_z*/,
0xfd /*guard_byte*/,
0 /*tseq-bits*/,
0 /*age-bits*/,
32 /*size-bits*/),
.debug_flag_ = false,
};
DArena arena = DArena::map(cfg);
REQUIRE(arena.reserved() >= cfg.size_);
REQUIRE(arena.committed() == 0);
REQUIRE(arena.available() == 0);
REQUIRE(arena.allocated() == 0);
size_t z0 = 1;
byte * m0 = arena.alloc(typeseq::sentinel(), 1);
REQUIRE(m0);
header_type* header = (header_type*)(m0 - sizeof(header_type));
REQUIRE(arena.contains(header));
REQUIRE(cfg.header_.size(*header) == padding::with_padding(z0));
//REQUIRE(((*header) & cfg.header_size_mask_) == padding::with_padding(z0));
REQUIRE(arena.last_error().error_ == error::ok);
REQUIRE(arena.last_error().error_seq_ == 0);
REQUIRE(arena.allocated() >= z0);
REQUIRE(arena.allocated() < sizeof(DArena::header_type) + z0 + padding::c_alloc_alignment );
REQUIRE(arena.allocated() <= arena.committed());
REQUIRE(arena.allocated() + arena.available() == arena.committed());
REQUIRE(arena.committed() <= arena.reserved());
}
}
}
/* end DArena.test.cpp */

View file

@ -1,360 +0,0 @@
/** @file DArenaHashMap.test.cpp
*
* @author Roland Conybeare, Jan 2026
**/
#include "DArenaHashMap.hpp"
#include "random_hash_ops.hpp"
#include <xo/randomgen/random_seed.hpp>
#include <xo/indentlog/scope.hpp>
#include <xo/indentlog/print/tag.hpp>
#include <xo/indentlog/print/hex.hpp>
#include <catch2/catch.hpp>
namespace xo {
using xo::map::DArenaHashMapUtil;
using xo::map::DArenaHashMap;
using xo::rng::random_seed;
using xo::rng::xoshiro256ss;
using utest::UtestTools;
using utest::HashMapUtil;
namespace ut {
TEST_CASE("DArenaHashMap-ctor", "[arena][DArenaHashMap]")
{
using HashMap = DArenaHashMap<int, int>;
HashMap map("utest");
REQUIRE(map.empty());
REQUIRE(map.size() == 0);
REQUIRE(map.groups() == 1);
REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size);
}
TEST_CASE("DArenaHashMap-ctor2", "[arena][DArenaHashMap]")
{
using HashMap = DArenaHashMap<int, int>;
HashMap map("utest", 257);
REQUIRE(map.empty());
REQUIRE(map.size() == 0);
REQUIRE(map.capacity() == map.groups() * DArenaHashMapUtil::c_group_size);
REQUIRE(map.capacity() == std::max(512ul,
DArenaHashMapUtil::c_group_size));
}
TEST_CASE("DArenaHashMap-try-insert", "[arena][DArenaHashMap]")
{
using HashMap = DArenaHashMap<int, int>;
HashMap map("utest");
REQUIRE(map.empty());
REQUIRE(map.size() == 0);
REQUIRE(map.groups() == 1);
REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size);
{
auto x = map.try_insert(std::make_pair(1, 11));
REQUIRE(x.first);
REQUIRE(x.second);
REQUIRE(!map.empty());
REQUIRE(map.size() == 1);
REQUIRE(map.groups() == 1);
REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size);
REQUIRE(map.load_factor() == 1/16.0);
/* verify iteration */
{
size_t n = 0;
for (auto & ix : map) {
REQUIRE(ix.first == 1);
REQUIRE(ix.second == 11);
++n;
}
REQUIRE(n == map.size());
}
REQUIRE(map.verify_ok(verify_policy::chatty()));
}
{
auto x = map.try_insert(std::make_pair(2, 9));
REQUIRE(x.first);
REQUIRE(x.second);
REQUIRE(!map.empty());
REQUIRE(map.size() == 2);
REQUIRE(map.groups() == 1);
REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size);
REQUIRE(map.load_factor() == 2/16.0);
/* verify iteration */
{
size_t n = 0;
for (auto & ix : map) {
++n;
}
REQUIRE(n == map.size());
}
REQUIRE(map.verify_ok(verify_policy::chatty()));
}
{
auto x = map.try_insert(std::make_pair(259, 12));
REQUIRE(x.first);
REQUIRE(x.second);
REQUIRE(!map.empty());
REQUIRE(map.size() == 3);
REQUIRE(map.groups() == 1);
REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size);
REQUIRE(map.load_factor() == 3/16.0);
/* verify iteration */
{
size_t n = 0;
for (auto & ix : map) {
switch (ix.first) {
case 1:
REQUIRE(ix.second == 11);
break;
case 2:
REQUIRE(ix.second == 9);
break;
case 259:
REQUIRE(ix.second == 12);
break;
default:
REQUIRE(false);
}
++n;
}
REQUIRE(n == map.size());
}
REQUIRE(map.verify_ok(verify_policy::chatty()));
}
{
map.clear();
REQUIRE(map.empty());
REQUIRE(map.size() == 0);
REQUIRE(map.groups() == 0);
REQUIRE(map.capacity() == 0);
REQUIRE(map.verify_ok(verify_policy::chatty()));
}
/* slightly different starting point, 0 capacity! */
{
auto x = map.try_insert(std::make_pair(1, 11));
/* try_insert should fail - no capacity */
REQUIRE(!x.first);
REQUIRE(!x.second);
REQUIRE(map.verify_ok(verify_policy::chatty()));
}
{
/* insert will grow hash table */
auto x = map.insert(std::make_pair(1, 11));
CHECK(x);
REQUIRE(!map.empty());
REQUIRE(map.size() == 1);
REQUIRE(map.groups() == 1);
REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size);
REQUIRE(map.load_factor() == 1/16.0);
/* verify iteration */
{
size_t n = 0;
for (auto & ix : map) {
REQUIRE(ix.first == 1);
REQUIRE(ix.second == 11);
++n;
}
REQUIRE(n == map.size());
}
REQUIRE(map.verify_ok(verify_policy::chatty()));
}
}
TEST_CASE("DArenaHashMap-try-insert2", "[arena][DArenaHashMap]")
{
using HashMap = DArenaHashMap<int, int>;
std::uint64_t seed = 17747889312058974961ul;
//random_seed(&seed); // to get new random seed
//log && log(xtag("seed", seed));
auto rgen = xoshiro256ss(seed);
/* 1. Perform series of tests with increasing scale
* 2. Each test may run in two modes:
* a. silent fast fail. just report success.
* In this mode avoid catch2 REQUIRE
* b. noisy. run with logging enabled
* This mode automatically invoked when silent mode
* observes test failure
*/
for (std::uint32_t n = 0; n <= 8; ) {
HashMap hash_map("utest");
auto test_fn = [&rgen, &hash_map](bool dbg_flag,
std::uint32_t n)
{
bool ok_flag = true;
ok_flag &= HashMapUtil<HashMap>::random_inserts(n, dbg_flag, &rgen, &hash_map);
ok_flag &= HashMapUtil<HashMap>::check_forward_iterator(0.0 /*dvalue*/,
dbg_flag, hash_map);
/* regular forward iterator, but start at hash_map.end() and use operator-- */
ok_flag &= HashMapUtil<HashMap>::check_backward_iterator(0.0 /*dvalue*/,
dbg_flag, hash_map);
ok_flag &= HashMapUtil<HashMap>::random_lookups(0.0 /*dvalue*/,
dbg_flag, &rgen, hash_map);
return ok_flag;
};
bool ok_flag = UtestTools::bimodal_test("DArenaHashMap-try-insert2", test_fn, n);
if (n == 0)
n = 1;
else
n = 2*n;
}
}
TEST_CASE("DArenaHashMap-operator-bracket", "[arena][DArenaHashMap]")
{
scope log(XO_DEBUG(false));
using HashMap = DArenaHashMap<int, int>;
HashMap map("utest");
// copy keys here so we can print stuff
std::vector<int> key_v;
// insert via operator[]
map[1] = 100;
key_v.push_back(1);
REQUIRE(map.verify_ok(verify_policy::chatty()));
map[2] = 200;
key_v.push_back(2);
REQUIRE(map.verify_ok(verify_policy::chatty()));
map[3] = 300;
key_v.push_back(3);
REQUIRE(map.verify_ok(verify_policy::chatty()));
REQUIRE(map.size() == 3);
// read back via operator[]
REQUIRE(map[1] == 100);
REQUIRE(map[2] == 200);
REQUIRE(map[3] == 300);
// update via operator[]
map[2] = 250;
REQUIRE(map[2] == 250);
REQUIRE(map.size() == 3); // size unchanged
REQUIRE(map.verify_ok(verify_policy::chatty()));
// verify via find
{
auto it = map.find(1);
REQUIRE(it != map.end());
REQUIRE(it->second == 100);
}
{
auto it = map.find(2);
REQUIRE(it != map.end());
REQUIRE(it->second == 250);
}
{
auto it = map.find(3);
REQUIRE(it != map.end());
REQUIRE(it->second == 300);
}
{
auto it = map.find(4);
REQUIRE(it == map.end());
}
REQUIRE(map.verify_ok(verify_policy::chatty()));
// operator[] on non-existent key creates default entry
int & val = map[999];
key_v.push_back(999);
for (uint64_t i_slot = 0, N = map._store()->n_slot_; i_slot < N; ++i_slot) {
auto key = map._store()->slots_[i_slot].first;
auto ctrl = map._store()->control_
[i_slot + DArenaHashMapUtil::c_control_stub];
auto isdata = DArenaHashMapUtil::is_data(ctrl);
auto [h1,h2] = map._hash(key);
if ((key != 0)
|| (h1 != 0)
|| (h2 != 0)
|| (ctrl != DArenaHashMapUtil::c_empty_slot)
|| isdata
) {
log && log(xtag("i", i_slot),
xtag("key[i]", key),
xtag("h1", h1), xtag("h2", h2),
xtag("ctrl[i]", (int)ctrl),
xtag("isdata", isdata));
}
}
REQUIRE(map.verify_ok(verify_policy::chatty()));
REQUIRE(map.size() == 4);
REQUIRE(val == 0); // default-initialized
val = 999;
REQUIRE(map[999] == 999);
}
TEST_CASE("DArenaHashMap-string_view-key", "[arena][DArenaHashMap]")
{
using HashMap = DArenaHashMap<std::string_view, int>;
HashMap map("utest", 1024);
REQUIRE(map.verify_ok());
map["hello"] = 42;
REQUIRE(map.size() == 1);
REQUIRE(map.verify_ok());
map["world"] = 100;
REQUIRE(map.size() == 2);
REQUIRE(map.verify_ok());
REQUIRE(map["hello"] == 42);
REQUIRE(map["world"] == 100);
}
// TODO:
// - let's try getting lcov to work in xo-umbrella2
}
}
/* end DArenaHashMap.test.cpp */

View file

@ -1,539 +0,0 @@
/** @file DArenaVector.test.cpp
*
* @author Roland Conybeare, Jan 2026
**/
#include "DArenaVector.hpp"
#include <catch2/catch.hpp>
namespace xo {
using xo::mm::DArenaVector;
using xo::mm::ArenaConfig;
using std::byte;
namespace ut {
TEST_CASE("DArenaVector-tiny", "[arena][DArenaVector]")
{
ArenaConfig cfg { .name_ = "testarena",
.size_ = 1 };
DArenaVector<double> arenavec = DArenaVector<double>::map(cfg);
REQUIRE(arenavec.empty());
}
TEST_CASE("DArenaVector-push_back-rvalue", "[arena][DArenaVector]")
{
ArenaConfig cfg { .name_ = "testarena",
.size_ = 4096 };
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
REQUIRE(vec.empty());
REQUIRE(vec.size() == 0);
vec.push_back(1.5);
REQUIRE(!vec.empty());
REQUIRE(vec.size() == 1);
REQUIRE(vec[0] == 1.5);
vec.push_back(2.5);
vec.push_back(3.5);
REQUIRE(vec.size() == 3);
REQUIRE(vec[0] == 1.5);
REQUIRE(vec[1] == 2.5);
REQUIRE(vec[2] == 3.5);
}
TEST_CASE("DArenaVector-push_back-lvalue", "[arena][DArenaVector]")
{
ArenaConfig cfg { .name_ = "testarena",
.size_ = 4096 };
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
double a = 10.0;
double b = 20.0;
double c = 30.0;
vec.push_back(a);
REQUIRE(vec.size() == 1);
REQUIRE(vec[0] == 10.0);
vec.push_back(b);
vec.push_back(c);
REQUIRE(vec.size() == 3);
REQUIRE(vec[0] == 10.0);
REQUIRE(vec[1] == 20.0);
REQUIRE(vec[2] == 30.0);
}
TEST_CASE("DArenaVector-at-valid", "[arena][DArenaVector]")
{
ArenaConfig cfg { .name_ = "testarena",
.size_ = 4096 };
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
vec.push_back(100.0);
vec.push_back(200.0);
vec.push_back(300.0);
REQUIRE(vec.at(0) == 100.0);
REQUIRE(vec.at(1) == 200.0);
REQUIRE(vec.at(2) == 300.0);
// test mutability via at()
vec.at(1) = 250.0;
REQUIRE(vec.at(1) == 250.0);
}
TEST_CASE("DArenaVector-at-throws", "[arena][DArenaVector]")
{
ArenaConfig cfg { .name_ = "testarena",
.size_ = 4096 };
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
// empty vector - any index is invalid
REQUIRE_THROWS_AS(vec.at(0), std::out_of_range);
vec.push_back(1.0);
vec.push_back(2.0);
// valid indices work
REQUIRE_NOTHROW(vec.at(0));
REQUIRE_NOTHROW(vec.at(1));
// index == size is invalid
REQUIRE_THROWS_AS(vec.at(2), std::out_of_range);
// index > size is invalid
REQUIRE_THROWS_AS(vec.at(100), std::out_of_range);
}
TEST_CASE("DArenaVector-resize-expand", "[arena][DArenaVector]")
{
ArenaConfig cfg { .name_ = "testarena",
.size_ = 4096 };
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
REQUIRE(vec.size() == 0);
// resize from 0 to 5
vec.resize(5);
REQUIRE(vec.size() == 5);
// can write to all indices
for (size_t i = 0; i < 5; ++i) {
vec[i] = static_cast<double>(i * 10);
}
REQUIRE(vec[0] == 0.0);
REQUIRE(vec[1] == 10.0);
REQUIRE(vec[2] == 20.0);
REQUIRE(vec[3] == 30.0);
REQUIRE(vec[4] == 40.0);
// resize to larger
vec.resize(8);
REQUIRE(vec.size() == 8);
// original values preserved
REQUIRE(vec[0] == 0.0);
REQUIRE(vec[1] == 10.0);
REQUIRE(vec[4] == 40.0);
}
TEST_CASE("DArenaVector-resize-shrink", "[arena][DArenaVector]")
{
ArenaConfig cfg { .name_ = "testarena",
.size_ = 4096 };
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
vec.push_back(1.0);
vec.push_back(2.0);
vec.push_back(3.0);
vec.push_back(4.0);
vec.push_back(5.0);
REQUIRE(vec.size() == 5);
// shrink to 3
vec.resize(3);
REQUIRE(vec.size() == 3);
// first 3 elements preserved
REQUIRE(vec[0] == 1.0);
REQUIRE(vec[1] == 2.0);
REQUIRE(vec[2] == 3.0);
// index 3 now out of bounds
REQUIRE_THROWS_AS(vec.at(3), std::out_of_range);
// shrink to 0
vec.resize(0);
REQUIRE(vec.size() == 0);
REQUIRE(vec.empty());
}
TEST_CASE("DArenaVector-resize-same", "[arena][DArenaVector]")
{
ArenaConfig cfg { .name_ = "testarena",
.size_ = 4096 };
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
vec.push_back(10.0);
vec.push_back(20.0);
vec.push_back(30.0);
REQUIRE(vec.size() == 3);
// resize to same size
vec.resize(3);
REQUIRE(vec.size() == 3);
// values unchanged
REQUIRE(vec[0] == 10.0);
REQUIRE(vec[1] == 20.0);
REQUIRE(vec[2] == 30.0);
}
TEST_CASE("DArenaVector-clear", "[arena][DArenaVector]")
{
ArenaConfig cfg { .name_ = "testarena",
.size_ = 4096 };
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
vec.push_back(1.0);
vec.push_back(2.0);
vec.push_back(3.0);
REQUIRE(vec.size() == 3);
REQUIRE(!vec.empty());
vec.clear();
REQUIRE(vec.size() == 0);
REQUIRE(vec.empty());
// can still push after clear
vec.push_back(99.0);
REQUIRE(vec.size() == 1);
REQUIRE(vec[0] == 99.0);
}
TEST_CASE("DArenaVector-iterators", "[arena][DArenaVector]")
{
ArenaConfig cfg { .name_ = "testarena",
.size_ = 4096 };
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
vec.push_back(10.0);
vec.push_back(20.0);
vec.push_back(30.0);
// begin/end
REQUIRE(vec.begin() != vec.end());
REQUIRE(vec.end() - vec.begin() == 3);
// iterate with pointer arithmetic
auto it = vec.begin();
REQUIRE(*it == 10.0);
++it;
REQUIRE(*it == 20.0);
++it;
REQUIRE(*it == 30.0);
++it;
REQUIRE(it == vec.end());
// modify through iterator
*vec.begin() = 15.0;
REQUIRE(vec[0] == 15.0);
}
TEST_CASE("DArenaVector-const-iterators", "[arena][DArenaVector]")
{
ArenaConfig cfg { .name_ = "testarena",
.size_ = 4096 };
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
vec.push_back(1.0);
vec.push_back(2.0);
vec.push_back(3.0);
const DArenaVector<double> & cvec = vec;
REQUIRE(cvec.cbegin() != cvec.cend());
REQUIRE(cvec.begin() == cvec.cbegin());
REQUIRE(cvec.end() == cvec.cend());
auto it = cvec.cbegin();
REQUIRE(*it == 1.0);
++it;
REQUIRE(*it == 2.0);
}
TEST_CASE("DArenaVector-range-for", "[arena][DArenaVector]")
{
ArenaConfig cfg { .name_ = "testarena",
.size_ = 4096 };
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
vec.push_back(1.0);
vec.push_back(2.0);
vec.push_back(3.0);
// read via range-for
double sum = 0.0;
for (double x : vec) {
sum += x;
}
REQUIRE(sum == 6.0);
// modify via range-for
for (double & x : vec) {
x *= 2.0;
}
REQUIRE(vec[0] == 2.0);
REQUIRE(vec[1] == 4.0);
REQUIRE(vec[2] == 6.0);
}
TEST_CASE("DArenaVector-reserve", "[arena][DArenaVector]")
{
ArenaConfig cfg { .name_ = "testarena",
.size_ = 4096 };
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
REQUIRE(vec.size() == 0);
REQUIRE(vec.capacity() > 0);
size_t initial_capacity = vec.capacity();
// reserve doesn't change size
vec.reserve(100);
REQUIRE(vec.size() == 0);
REQUIRE(vec.capacity() >= 100);
// add some elements
vec.push_back(1.0);
vec.push_back(2.0);
vec.push_back(3.0);
REQUIRE(vec.size() == 3);
size_t cap_after_push = vec.capacity();
// reserve more space
vec.reserve(200);
REQUIRE(vec.size() == 3);
REQUIRE(vec.capacity() >= 200);
// values still intact
REQUIRE(vec[0] == 1.0);
REQUIRE(vec[1] == 2.0);
REQUIRE(vec[2] == 3.0);
}
TEST_CASE("DArenaVector-swap", "[arena][DArenaVector]")
{
ArenaConfig cfg1 { .name_ = "testarena1",
.size_ = 4096 };
ArenaConfig cfg2 { .name_ = "testarena2",
.size_ = 4096 };
DArenaVector<double> vec1 = DArenaVector<double>::map(cfg1);
DArenaVector<double> vec2 = DArenaVector<double>::map(cfg2);
vec1.push_back(1.0);
vec1.push_back(2.0);
vec2.push_back(10.0);
vec2.push_back(20.0);
vec2.push_back(30.0);
REQUIRE(vec1.size() == 2);
REQUIRE(vec2.size() == 3);
vec1.swap(vec2);
// sizes swapped
REQUIRE(vec1.size() == 3);
REQUIRE(vec2.size() == 2);
// contents swapped
REQUIRE(vec1[0] == 10.0);
REQUIRE(vec1[1] == 20.0);
REQUIRE(vec1[2] == 30.0);
REQUIRE(vec2[0] == 1.0);
REQUIRE(vec2[1] == 2.0);
}
TEST_CASE("DArenaVector-data", "[arena][DArenaVector]")
{
ArenaConfig cfg { .name_ = "testarena",
.size_ = 4096 };
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
vec.push_back(1.0);
vec.push_back(2.0);
vec.push_back(3.0);
double * ptr = vec.data();
// data() points to first element
REQUIRE(ptr == &vec[0]);
// can read via pointer
REQUIRE(ptr[0] == 1.0);
REQUIRE(ptr[1] == 2.0);
REQUIRE(ptr[2] == 3.0);
// can write via pointer
ptr[1] = 99.0;
REQUIRE(vec[1] == 99.0);
// const version
const DArenaVector<double> & cvec = vec;
const double * cptr = cvec.data();
REQUIRE(cptr[0] == 1.0);
REQUIRE(cptr[1] == 99.0);
REQUIRE(cptr[2] == 3.0);
}
TEST_CASE("DArenaVector-move-ctor", "[arena][DArenaVector]")
{
ArenaConfig cfg { .name_ = "testarena",
.size_ = 4096 };
DArenaVector<double> vec1 = DArenaVector<double>::map(cfg);
vec1.push_back(10.0);
vec1.push_back(20.0);
vec1.push_back(30.0);
double * original_data = vec1.data();
size_t original_size = vec1.size();
// move construct vec2 from vec1
DArenaVector<double> vec2(std::move(vec1));
// vec2 has the data
REQUIRE(vec2.size() == original_size);
REQUIRE(vec2.data() == original_data);
REQUIRE(vec2[0] == 10.0);
REQUIRE(vec2[1] == 20.0);
REQUIRE(vec2[2] == 30.0);
// vec1 is in valid but moved-from state
REQUIRE(vec1.size() == 0);
REQUIRE(vec1.empty());
}
// Helper class to track ctor/dtor calls
struct LifetimeTracker {
static int ctor_count;
static int dtor_count;
static void reset() {
ctor_count = 0;
dtor_count = 0;
}
int value;
LifetimeTracker() : value{0} { ++ctor_count; }
LifetimeTracker(int v) : value{v} { ++ctor_count; }
LifetimeTracker(const LifetimeTracker & other) : value{other.value} { ++ctor_count; }
LifetimeTracker(LifetimeTracker && other) : value{other.value} { ++ctor_count; other.value = 0; }
~LifetimeTracker() { ++dtor_count; }
LifetimeTracker & operator=(const LifetimeTracker &) = default;
LifetimeTracker & operator=(LifetimeTracker &&) = default;
};
int LifetimeTracker::ctor_count = 0;
int LifetimeTracker::dtor_count = 0;
TEST_CASE("DArenaVector-nontrivial-dtor", "[arena][DArenaVector]")
{
LifetimeTracker::reset();
{
ArenaConfig cfg { .name_ = "testarena",
.size_ = 4096 };
DArenaVector<LifetimeTracker> vec = DArenaVector<LifetimeTracker>::map(cfg);
vec.push_back(LifetimeTracker{1});
vec.push_back(LifetimeTracker{2});
vec.push_back(LifetimeTracker{3});
// 3 temp objects created, 3 moved into vector
// temps destroyed after push_back
REQUIRE(vec.size() == 3);
REQUIRE(LifetimeTracker::ctor_count == 6); // 3 temps + 3 moves
REQUIRE(LifetimeTracker::dtor_count == 3); // 3 temps destroyed
// verify values
REQUIRE(vec[0].value == 1);
REQUIRE(vec[1].value == 2);
REQUIRE(vec[2].value == 3);
}
// vec destroyed, should call dtor for all 3 elements
REQUIRE(LifetimeTracker::dtor_count == 6);
}
TEST_CASE("DArenaVector-nontrivial-resize-shrink", "[arena][DArenaVector]")
{
LifetimeTracker::reset();
ArenaConfig cfg { .name_ = "testarena",
.size_ = 4096 };
DArenaVector<LifetimeTracker> vec = DArenaVector<LifetimeTracker>::map(cfg);
vec.push_back(LifetimeTracker{1});
vec.push_back(LifetimeTracker{2});
vec.push_back(LifetimeTracker{3});
vec.push_back(LifetimeTracker{4});
vec.push_back(LifetimeTracker{5});
REQUIRE(vec.size() == 5);
int dtors_before_shrink = LifetimeTracker::dtor_count;
// shrink from 5 to 2
vec.resize(2);
REQUIRE(vec.size() == 2);
// should have called dtor for 3 elements (indices 2,3,4)
REQUIRE(LifetimeTracker::dtor_count == dtors_before_shrink + 3);
// remaining elements intact
REQUIRE(vec[0].value == 1);
REQUIRE(vec[1].value == 2);
}
TEST_CASE("DArenaVector-nontrivial-clear", "[arena][DArenaVector]")
{
LifetimeTracker::reset();
ArenaConfig cfg { .name_ = "testarena",
.size_ = 4096 };
DArenaVector<LifetimeTracker> vec = DArenaVector<LifetimeTracker>::map(cfg);
vec.push_back(LifetimeTracker{1});
vec.push_back(LifetimeTracker{2});
vec.push_back(LifetimeTracker{3});
REQUIRE(vec.size() == 3);
int dtors_before_clear = LifetimeTracker::dtor_count;
vec.clear();
REQUIRE(vec.size() == 0);
REQUIRE(vec.empty());
// should have called dtor for all 3 elements
REQUIRE(LifetimeTracker::dtor_count == dtors_before_clear + 3);
}
}
}
/* end DArenaVector.test.cpp */

View file

@ -1,62 +0,0 @@
/** @file DCircularBuffer.test.cpp
*
* @author Roland Conybeare, Jan 2026
**/
#include "DCircularBuffer.hpp"
#include "print.hpp"
#include <xo/indentlog/print/tag.hpp>
#include <catch2/catch.hpp>
#include <unistd.h> // for getpagesize() on osx
namespace xo {
using xo::mm::DCircularBuffer;
using xo::mm::CircularBufferConfig;
using xo::mm::span;
using std::byte;
namespace ut {
TEST_CASE("DCircularBuffer-tiny", "[arena][DCircularBuffer]")
{
// buffer works with bytes, not chars
CircularBufferConfig cfg { .name_ = "testcbuf",
.max_capacity_ = 1 };
DCircularBuffer buf = DCircularBuffer::map(cfg);
REQUIRE(buf.reserved_range().size() == getpagesize());
REQUIRE(buf.mapped_range().size() == 0);
REQUIRE(buf.occupied_range().size() == 0);
REQUIRE(buf.input_range().size() == 0);
REQUIRE(buf.verify_ok(verify_policy::log_only()));
REQUIRE(buf.get_append_span(1).size() == getpagesize());
REQUIRE(buf.mapped_range().size() == getpagesize());
REQUIRE(buf.occupied_range().size() == 0);
REQUIRE(buf.input_range().size() == 0);
auto s0 = DCircularBuffer::const_span_type::from_cstr("abcdefghijk");
/* return value is unaccepted suffix of input */
REQUIRE(buf.append(s0).empty());
REQUIRE(buf.verify_ok(verify_policy::log_only()));
REQUIRE(buf.mapped_range().size() == getpagesize());
REQUIRE(buf.occupied_range().size() == s0.size());
REQUIRE(buf.input_range().size() == s0.size());
auto s1 = DCircularBuffer::const_span_type::from_cstr("lmnopq");
REQUIRE(buf.append(s1).empty());
REQUIRE(buf.mapped_range().size() == getpagesize());
REQUIRE(buf.occupied_range().size() == s0.size() + s1.size());
REQUIRE(buf.occupied_range().to_string_view() == std::string_view("abcdefghijklmnopq"));
buf.consume(buf.occupied_range().prefix(3));
REQUIRE(buf.occupied_range().to_string_view() == std::string_view("defghijklmnopq"));
}
// TODO: test pin_range() / unpin_range()
} /*namespace ut*/
} /*namespace xo*/
/* end DCircularBuffer.test.cpp */

View file

@ -1,6 +0,0 @@
/* file arena_utest_main.cpp */
#define CATCH_CONFIG_MAIN
#include "catch2/catch.hpp"
/* end arena_utest_main.cpp */

View file

@ -1,714 +0,0 @@
/* @file random_hash_ops.hpp **/
#include <xo/randomgen/xoshiro256.hpp>
#include <xo/indentlog/scope.hpp>
#include <xo/indentlog/print/tag.hpp>
#include <xo/indentlog/print/vector.hpp>
#include <catch2/catch.hpp>
#include <algorithm>
#include <map>
#include <unordered_set>
#include <vector>
namespace utest {
struct Util {
/* generate vector with integers [0.. n-1] */
static std::vector<std::uint32_t> vector_upto(std::uint32_t n) {
std::vector<std::uint32_t> u(n);
for (std::uint32_t i = 0; i < n; ++i)
u[i] = i;
return u;
} /*vector_upto*/
static std::map<std::uint32_t, std::uint32_t>
map_upto(std::uint32_t n)
{
std::map<std::uint32_t, std::uint32_t> m;
for(std::uint32_t i=0; i<n; ++i) {
m[i] = i;
}
return m;
} /*map_upto*/
/* generate random permutation of integers [0.. n-1] */
static std::vector<uint32_t>
random_permutation(uint32_t n, xo::rng::xoshiro256ss *p_rgen) {
/* vector [0 .. n-1] */
std::vector<uint32_t> u = vector_upto(n);
/* shuffle to get unpredictable permutation */
std::shuffle(u.begin(), u.end(), *p_rgen);
return u;
} /*random_permutation*/
}; /*Util*/
// TODO: move REQUIRE_OR_CAPTURE(), REQUIRE_ORFAIL() to new subsystem xo-utestutil
/* note: trivial REQUIRE() call in else branch bc we still want
* catch2 to count assertions when verification succeeds
*/
# define REQUIRE_ORCAPTURE(ok_flag, catch_flag, expr) \
if (catch_flag) { \
REQUIRE((expr)); \
} else { \
REQUIRE(true); \
ok_flag &= (expr); \
}
# define REQUIRE_ORFAIL(ok_flag, catch_flag, expr) \
REQUIRE_ORCAPTURE(ok_flag, catch_flag, expr); \
if (!ok_flag) \
return ok_flag
/** UtestTools
**/
struct UtestTools {
/** bimodal may run twice:
* - first mode is silent, only determines success or failure.
* - second mode skipped when first mode succeeds.
* when first mode fails, second mode runs noisily with debug logging enabled
*
* goal is to get detailed information from failing test;
* more detailed than feasible from catch2 INFO()
*
* test function should use REQUIRE_ORCAPTURE() / REQUIRE_ORFAIL().
* It should *not* use REQUIRE() or CHECK().
*
* @p test_name banner for initial log message (only printed on 2nd pass)
* @p test_fn function to invoke test pass.
* @p n test size/id (cosmetic - printed in log messages)
**/
static inline bool bimodal_test(std::string test_name,
std::function<bool (bool dbg_flag, std::uint32_t n)> test_fn,
std::uint32_t n)
{
bool ok_flag = false;
for (std::uint32_t attention = 0; !ok_flag && (attention < 2); ++attention) {
bool debug_flag = (attention == 1);
xo::scope log(XO_DEBUG2(debug_flag, test_name));
ok_flag = test_fn(debug_flag, n);
}
return ok_flag;
}
};
/* compare xo-ordinaltree/utest/random_tree_ops.hpp */
template <typename HashMap>
struct HashMapUtil : public Util {
#ifdef NOT_YET
static bool
test_clear(bool catch_flag,
Tree * p_tree)
{
bool ok_flag = true;
REQUIRE_ORFAIL(ok_flag, catch_flag, p_tree->verify_ok(catch_flag));
p_tree->clear();
REQUIRE_ORFAIL(ok_flag, catch_flag, p_tree->verify_ok(catch_flag));
REQUIRE_ORFAIL(ok_flag, catch_flag, p_tree->empty());
REQUIRE_ORFAIL(ok_flag, catch_flag, p_tree->size() == 0);
return ok_flag;
} /*test_clear*/
#endif
static bool
random_inserts(const std::vector<typename HashMap::key_type> & keys,
bool catch_flag,
xo::rng::xoshiro256ss * p_rgen,
HashMap * p_map)
{
using xo::xtag;
bool ok_flag = true;
xo::scope log(XO_DEBUG(catch_flag), xtag("n-keys", keys.size()));
REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->verify_ok(catch_flag));
/* n keys */
std::size_t n = keys.size();
/* permute keys, remembering original position */
std::vector<std::pair<std::size_t, typename HashMap::key_type>> permuted_keys(n);
{
uint32_t i = 0;
for (const auto & x : keys) {
permuted_keys[i] = std::make_pair(i, x);
}
}
/* shuffle to get unpredictable insert order */
std::shuffle(keys.begin(), keys.end(), *p_rgen);
size_t tree_z0 = p_map->size();
/* insert keys in permuted order */
{
uint32_t i = 1;
for(const auto & pr_i : permuted_keys) {
log && log(xtag("i", i), xtag("ord", pr_i.first), xtag("n", n), xtag("key", pr_i.second));
/* .first: iterator @ insert position
* .second: true if insert occurred ( tree size incremented)
*/
auto insert_result = p_map->insert(typename HashMap::value_type(pr_i.second, 10.0 * i));
REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->verify_ok(catch_flag));
REQUIRE_ORFAIL(ok_flag, catch_flag, insert_result.second);
/* verify: iterator returned by Treẹinsert(), refers to inserted key,value pair */
log && log(xtag("iter.node", insert_result.first.node()));
REQUIRE_ORFAIL(ok_flag, catch_flag, insert_result.first->first == pr_i.second);
REQUIRE_ORFAIL(ok_flag, catch_flag, insert_result.first->second == 10.0 * i);
++i;
}
}
REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->size() == tree_z0 + n);
return ok_flag;
}
/* do
* n = (hi - lo) / k
* random inserts (taken from *p_rgen) into *p_rbtreẹ
* inserted keys will comprise the distinct values
* {lo, lo+k, lo+2k, ..., lo+n.k}
*/
static bool
random_inserts(std::uint32_t lo,
std::uint32_t hi,
std::uint32_t k,
bool catch_flag,
xo::rng::xoshiro256ss * p_rgen,
HashMap * p_map)
{
// TODO: rewrite in terms of 'random_inserts with explicit vector'.
using xo::xtag;
bool ok_flag = true;
xo::scope log(XO_DEBUG(catch_flag), xtag("lo", lo), xtag("hi", hi), xtag("k", k));
auto policy = xo::verify_policy::chatty();
REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->verify_ok(policy));
if ((hi <= lo) || (k == 0))
return true;
uint32_t n = (hi - lo) / k;
/* n keys 0..n-1 */
std::vector<std::uint32_t> u(n);
for(std::uint32_t i=0; i<n; ++i)
u[i] = lo + i*k;
/* shuffle to get unpredictable insert order */
std::shuffle(u.begin(), u.end(), *p_rgen);
size_t tree_z0 = p_map->size();
/* insert keys according to permutation u */
uint32_t i = 1;
for(uint32_t x : u) {
log && log(xtag("i", i), xtag("n", n), xtag("key", x));
/* .first: iterator @ insert position
* .second: true if insert occurred ( tree size incremented)
*/
auto insert_result = p_map->try_insert(typename HashMap::value_type(x, 10 * x));
REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->verify_ok(policy));
REQUIRE_ORFAIL(ok_flag, catch_flag, insert_result.second);
/* verify: iterator returned by Treẹinsert(), refers to inserted key,value pair */
//log && log(xtag("iter.node", insert_result.first.node()));
REQUIRE_ORFAIL(ok_flag, catch_flag, insert_result.first->first == x);
REQUIRE_ORFAIL(ok_flag, catch_flag, insert_result.first->second == 10 * x);
++i;
}
REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->size() == tree_z0 + n);
return ok_flag;
} /*random_inserts*/
static bool
random_inserts(std::uint32_t n,
bool catch_flag,
xo::rng::xoshiro256ss * p_rgen,
HashMap * p_map)
{
return random_inserts(0, n, 1, catch_flag, p_rgen, p_map);
}
#ifdef NOT_YET
/* do n random removes (taken from *p_rgen) from *p_rbtree;
* assumes *p_rbtree has keys [0 .. n-1] where n=p_rbtreẹsize
*/
static bool
random_removes(bool catch_flag, // dbg_flag
xo::rng::xoshiro256ss * p_rgen,
Tree * p_map)
{
using xo::scope;
using xo::xtag;
bool ok_flag = true;
xo::scope log(XO_DEBUG(catch_flag));
REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->verify_ok(catch_flag));
uint32_t n = p_map->size();
/* random permutation of keys in *p_map */
std::vector<std::uint32_t> u
= random_permutation(n, p_rgen);
log && log(xtag("remove-order", u));
/* will keep track of which keys remain as we move them */
std::map<std::uint32_t, std::uint32_t> m = Util::map_upto(n);
/* remove keys in permutation order */
std::uint32_t i = 1;
for (std::uint32_t x : u) {
log && log("iter i: removing key from n-node tree",
xtag("i", i), xtag("key", x), xtag("n", n));
/* remove x from tracking map m also */
m.erase(x);
log && log("remove key :iter ", i, "/", n, xtag("key", x));
p_map->erase(x);
// rbtreẹdisplay();
REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->size() == n-i);
/* amongst other things, this guarantees that keys in *p_map
* appear in increasing order
*/
REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->verify_ok(catch_flag));
#ifdef NOT_YET
/* 1. rbtree should now contain all the keys in [0..n-1],
* with u[0]..u[i-1] excluded; this is the same as the
* contents of m.
*/
auto m_ix = m.begin();
auto m_end_ix = m.end();
auto visitor_fn =
([&m_ix, m_end_ix]
(std::pair<int, double> const & contents)
{
REQUIRE(m_ix != m_end_ix);
REQUIRE(contents.first == m_ix->second);
++m_ix;
});
p_map->visit_inorder(visitor_fn);
#endif
++i;
}
REQUIRE_ORFAIL(ok_flag, catch_flag, m.empty());
REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->size() == 0);
log.end_scope();
return ok_flag;
} /*random_removes*/
#endif
/* Require:
* - map has keys [0..n-1], where n=map.size()
* - for each key k, associated value is dvalue+10*k
*/
static bool
random_lookups(uint32_t dvalue,
bool catch_flag,
xo::rng::xoshiro256ss * p_rgen,
HashMap & map)
{
using xo::scope;
using xo::xtag;
xo::scope log(XO_DEBUG(catch_flag));
/* -> false if/when verification fails */
bool ok_flag = true;
REQUIRE_ORFAIL(ok_flag, catch_flag, map.verify_ok());
size_t n = map.size();
std::vector<std::uint32_t> u
= random_permutation(n, p_rgen);
/* lookup keys in permutation order */
std::uint32_t i = 1;
for (std::uint32_t x : u) {
INFO(tostr(xtag("i", i), xtag("n", n), xtag("x", x)));
auto find_ix = map.find(x);
REQUIRE_ORFAIL(ok_flag, catch_flag, find_ix != map.end());
REQUIRE_ORFAIL(ok_flag, catch_flag, find_ix->first == x);
REQUIRE_ORFAIL(ok_flag, catch_flag, find_ix->second == dvalue + x*10);
REQUIRE_ORFAIL(ok_flag, catch_flag, map.verify_ok());
REQUIRE_ORFAIL(ok_flag, catch_flag, map.size() == n);
++i;
}
REQUIRE_ORFAIL(ok_flag, catch_flag, map.size() == n);
log.end_scope();
return ok_flag;
} /*random_lookups*/
/* Require:
* - hash has keys [0..n-1] where n=map size
* - hash value at key k is dvalue+10*k
*/
static bool
check_forward_iterator(uint32_t dvalue,
bool catch_flag,
HashMap & map)
{
using xo::scope;
using xo::xtag;
/* -> flase if/when verification fails */
bool ok_flag = true;
std::size_t const n = map.size();
scope log(XO_DEBUG(catch_flag));
log && log("map with size n", xtag("n", n));
std::unordered_set<std::size_t> keys;
{
auto end_ix = map.end();
//log && log(xtag("end_ix", end_ix));
auto begin_ix = map.begin();
auto ix = begin_ix;
int last_key = -1;
while (ix != end_ix) {
log && log("forward loop top"
//xtag("ix", ix)
);
/* verify: keys in map are in [0 .. n) */
REQUIRE_ORFAIL(ok_flag, catch_flag, 0 <= ix->first);
REQUIRE_ORFAIL(ok_flag, catch_flag, ix->first < n);
/* verify: keys in map are unique */
REQUIRE_ORFAIL(ok_flag, catch_flag, !keys.contains(ix->first));
keys.insert(ix->first);
REQUIRE_ORFAIL(ok_flag, catch_flag, ix->second == dvalue + 10 * ix->first);
last_key = ix->first;
++ix;
log && log("forward loop bottom",
xtag("last_key", last_key)
//xtag("next ix", ix)
);
}
/* should have visited exactly n locations */
REQUIRE_ORFAIL(ok_flag, catch_flag, map.size() == keys.size());
REQUIRE_ORFAIL(ok_flag, catch_flag, ix == end_ix);
//log && log(xtag("ix", ix), xtag("begin_ix", begin_ix));
}
return ok_flag;
}
/* Require:
* - hash has keys [0..n-1] where n=map size
* - hash value at key k is dvalue+10*k
*/
static bool
check_backward_iterator(uint32_t dvalue,
bool catch_flag,
HashMap & map)
{
using xo::scope;
using xo::xtag;
/* -> flase if/when verification fails */
bool ok_flag = true;
std::size_t const n = map.size();
scope log(XO_DEBUG(catch_flag));
log && log("map with size n", xtag("n", n));
std::unordered_set<std::size_t> keys;
{
auto end_ix = map.end();
//log && log(xtag("end_ix", end_ix));
auto begin_ix = map.begin();
auto ix = end_ix;
if (ix == begin_ix) [[unlikely]] {
return ok_flag;
}
while (ix != begin_ix) {
log && log("backward loop top",
xtag("n", n)
);
--ix;
/* verify: keys in map are in [0 .. n) */
REQUIRE_ORFAIL(ok_flag, catch_flag, 0 <= ix->first);
REQUIRE_ORFAIL(ok_flag, catch_flag, ix->first < n);
log && log(xtag("ix->first", ix->first));
/* verify: keys in map are unique */
REQUIRE_ORFAIL(ok_flag, catch_flag, !keys.contains(ix->first));
keys.insert(ix->first);
REQUIRE_ORFAIL(ok_flag, catch_flag, ix->second == dvalue + 10 * ix->first);
}
/* should have visited exactly n locations */
REQUIRE_ORFAIL(ok_flag, catch_flag, map.size() == keys.size());
REQUIRE_ORFAIL(ok_flag, catch_flag, ix == begin_ix);
//log && log(xtag("ix", ix), xtag("begin_ix", begin_ix));
}
return ok_flag;
}
#ifdef NOT_YET
/* Require:
* - tree has keys [0..n-1], where n=treẹsize()
* - tree values at key k is dvalue+10*k
*
* catch_flag. true -> log to console + interact with catch2
* false -> verify iteration behavior for return code
*/
static bool
check_bidirectional_iterator(uint32_t dvalue,
bool catch_flag,
Tree const & tree)
{
using xo::scope;
using xo::xtag;
/* -> false if/when verification fails */
bool ok_flag = true;
std::size_t const n = tree.size();
xo::scope log(XO_DEBUG(catch_flag));
log && log("tree with size n", xtag("n", n));
{
std::size_t i = 0;
auto end_ix = tree.end();
log && log(xtag("end_ix", end_ix));
auto begin_ix = tree.begin();
auto ix = begin_ix;
int last_key = -1;
while (ix != end_ix) {
log && log("forward loop top",
xtag("i", i),
xtag("ix", ix));
REQUIRE_ORFAIL(ok_flag, catch_flag, ix->first == i);
REQUIRE_ORFAIL(ok_flag, catch_flag, ix->second == dvalue + 10*i);
if(i > 0) {
REQUIRE_ORFAIL(ok_flag, catch_flag, ix->first > last_key);
}
last_key = ix->first;
++i;
++ix;
log && log("forward loop bottom",
xtag("last_key", last_key),
xtag("next ix", ix));
}
/* should have visited exactly n locations */
REQUIRE_ORFAIL(ok_flag, catch_flag, i == n);
REQUIRE_ORFAIL(ok_flag, catch_flag, ix == end_ix);
log && log(xtag("ix", ix), xtag("begin_ix", begin_ix));
/* now run iterator backwards,
* starting from "one past the end"
*/
if(ix != begin_ix) {
do {
--i;
--ix;
log && log("forward backup",
xtag("i", i),
xtag("ix", ix));
REQUIRE_ORFAIL(ok_flag, catch_flag, ix.is_dereferenceable());
log && log(xtag("ix.first", (*ix).first));
REQUIRE_ORFAIL(ok_flag, catch_flag, (*ix).first == i);
} while (ix != begin_ix);
}
/* should have visited exactly n locations in reverse */
REQUIRE_ORFAIL(ok_flag, catch_flag, i == 0);
}
/* ----- reverse iterators ----- */
{
std::int64_t i = n - 1;
auto rbegin_ix = tree.rbegin();
auto rend_ix = tree.rend();
auto rix = rbegin_ix;
int last_key = -1;
while (rix != rend_ix) {
log && log("reverse loop top",
xtag("i", i),
xtag("rix", rix));
REQUIRE_ORFAIL(ok_flag, catch_flag, rix->first == i);
REQUIRE_ORFAIL(ok_flag, catch_flag, rix->second == dvalue + 10*i);
if (i < n-1) {
REQUIRE_ORFAIL(ok_flag, catch_flag, rix->first < last_key);
}
last_key = rix->first;
--i;
++rix;
log && log("reverse loop bottom",
xtag("last_key", last_key),
xtag("next ix", rix));
}
/* should have visited exactly n locations */
REQUIRE_ORFAIL(ok_flag, catch_flag, i == -1);
log && log(xtag("rbegin_ix", rbegin_ix));
/* now run reverse iterator backwrds,
* starting from "one before the beginning"
*/
if (rix != rbegin_ix) {
do {
++i;
--rix;
log && log("reverse backup",
xtag("i", i),
xtag("rix", rix),
xtag("rix.first", rix->first));
REQUIRE_ORFAIL(ok_flag, catch_flag, (*rix).first == i);
} while (rix != rbegin_ix);
}
/* should have visited exactly n locations in reversê2 */
REQUIRE_ORFAIL(ok_flag, catch_flag, i == n - 1);
}
log.end_scope();
return ok_flag;
} /*check_bidirectional_iterator*/
#endif
#ifdef NOT_YET
/* Require:
* - *p_rbtree has keys [0..n-1], where n=rbtree.size()
* - for each key k, associated value is 10*k
*
* Promise:
* - for each key k, associated value is dvalue + 10*k
*/
static bool
random_updates(uint32_t dvalue,
bool catch_flag,
Tree * p_rbtree,
xo::rng::xoshiro256ss * p_rgen)
{
using xo::scope;
using xo::xtag;
scope log(XO_DEBUG(catch_flag));
/* -> false if/when check fails */
bool ok_flag = true;
REQUIRE_ORFAIL(ok_flag, catch_flag, p_rbtree->verify_ok());
std::size_t n = p_rbtree->size();
std::vector<uint32_t> u
= Util::random_permutation(n, p_rgen);
/* update key/value pairs in permutation order */
uint32_t i = 1;
for (uint32_t x : u) {
REQUIRE_ORFAIL(ok_flag, catch_flag, (*p_rbtree)[x] == x*10);
(*p_rbtree)[x] = dvalue + 10*x;
REQUIRE_ORFAIL(ok_flag, catch_flag, (*p_rbtree)[x] == dvalue + 10*x);
REQUIRE_ORFAIL(ok_flag, catch_flag, p_rbtree->verify_ok());
/* assignment to existing key does not change tree size */
REQUIRE_ORFAIL(ok_flag, catch_flag, p_rbtree->size() == n);
++i;
}
REQUIRE(p_rbtree->size() == n);
return ok_flag;
} /*random_updates*/
#endif
}; /*TreeUtil*/
} /*namespace utest*/
/* end random_tree_ops.hpp */