// SPDX-FileCopyrightText: © 2026 Vladimir Zorin // SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later // Licensed under OWL v1.0+. See LICENSE. #include "mneme.h" #include #include #include static int page_list_append(uint32_t **pages, int *count, int *cap, uint32_t pgno) { if (*count >= *cap) { int new_cap = *cap == 0 ? 16 : *cap * 2; uint32_t *new_arr = (uint32_t *)realloc(*pages, (size_t)new_cap * sizeof(uint32_t)); if (!new_arr) return -1; *pages = new_arr; *cap = new_cap; } (*pages)[(*count)++] = pgno; return 0; } static int page_list_remove(uint32_t *pages, int *count, uint32_t pgno) { for (int i = 0; i < *count; i++) { if (pages[i] != pgno) continue; pages[i] = pages[*count - 1]; (*count)--; return 0; } return -1; } static void page_fresh_tail_shrink(mneme_txn_t *txn) { while (txn->meta.page_count > 0) { uint32_t tail_pgno = (uint32_t)(txn->meta.page_count - 1); if (page_list_remove(txn->fresh_free_pages, &txn->fresh_free_count, tail_pgno) < 0) break; txn->meta.page_count--; } } /* ── Dirty page management ─────────────────────────────────────────── */ uint8_t *mneme_dirty_find(mneme_txn_t *txn, uint32_t pgno) { for (int i = 0; i < txn->dirty_count; i++) { if (txn->dirty[i].pgno == pgno) return txn->dirty[i].data; } return NULL; } static mneme_dirty_page_t *dirty_find_entry(mneme_txn_t *txn, uint32_t pgno) { for (int i = 0; i < txn->dirty_count; i++) { if (txn->dirty[i].pgno == pgno) return &txn->dirty[i]; } return NULL; } static mneme_dirty_page_t *dirty_alloc(mneme_txn_t *txn, uint32_t pgno, mneme_dirty_origin_t origin) { /* Check if already dirty */ mneme_dirty_page_t *existing = dirty_find_entry(txn, pgno); if (existing) return existing; /* Grow array if needed. dirty_cap can be 0 on a readonly or finished txn, where 0*2 would make realloc return a 0-byte block that the entry write below would overrun. */ if (txn->dirty_count >= txn->dirty_cap) { int new_cap = (txn->dirty_cap == 0) ? MNEME_DIRTY_INIT : txn->dirty_cap * 2; mneme_dirty_page_t *new_arr = (mneme_dirty_page_t *)realloc(txn->dirty, (size_t)new_cap * sizeof(mneme_dirty_page_t)); if (!new_arr) return NULL; txn->dirty = new_arr; txn->dirty_cap = new_cap; } mneme_dirty_page_t *dp = &txn->dirty[txn->dirty_count++]; dp->pgno = pgno; dp->origin = origin; return dp; } /* ── Page resolve (dirty copy or mmap) ─────────────────────────────── */ const uint8_t *mneme_page_resolve(mneme_txn_t *txn, uint32_t pgno) { if (txn->readonly) return mneme_page_read(txn->db, pgno); uint8_t *dirty = mneme_dirty_find(txn, pgno); if (dirty) return dirty; return mneme_page_read(txn->db, pgno); } /* ── Read a page from mmap ─────────────────────────────────────────── */ const uint8_t *mneme_page_read(mneme_db_t *db, uint32_t pgno) { size_t off = (size_t)pgno * MNEME_PAGE_SIZE; if (off + MNEME_PAGE_SIZE > db->map_size) return NULL; return db->map + off; } /* ── Get a zeroed writable page copy ───────────────────────────────── */ static uint8_t *mneme_page_write_zero(mneme_txn_t *txn, uint32_t pgno, mneme_dirty_origin_t origin) { uint8_t *existing = mneme_dirty_find(txn, pgno); if (existing) { memset(existing, 0, MNEME_PAGE_SIZE); return existing; } mneme_dirty_page_t *dp = dirty_alloc(txn, pgno, origin); if (!dp) return NULL; memset(dp->data, 0, MNEME_PAGE_SIZE); return dp->data; } /* ── Allocate a page ───────────────────────────────────────────────── */ uint32_t mneme_page_alloc(mneme_txn_t *txn) { if (txn->fresh_free_count > 0) { uint32_t pgno = txn->fresh_free_pages[--txn->fresh_free_count]; uint8_t *wp = mneme_page_write_zero(txn, pgno, MNEME_DIRTY_ALLOCATED); if (!wp) return 0; return pgno; } if (txn->reuse_count > 0) { uint32_t pgno = txn->reuse_pages[--txn->reuse_count]; if (page_list_append(&txn->consumed_reuse_pages, &txn->consumed_reuse_count, &txn->consumed_reuse_cap, pgno) < 0) { txn->reuse_pages[txn->reuse_count++] = pgno; return 0; } uint8_t *wp = mneme_page_write_zero(txn, pgno, MNEME_DIRTY_REUSED); if (!wp) { txn->consumed_reuse_count--; txn->reuse_pages[txn->reuse_count++] = pgno; return 0; } return pgno; } uint32_t pgno = (uint32_t)txn->meta.page_count; txn->meta.page_count++; uint8_t *wp = mneme_page_write_zero(txn, pgno, MNEME_DIRTY_ALLOCATED); if (!wp) { txn->meta.page_count--; return 0; } return pgno; } int mneme_page_discard_dirty(mneme_txn_t *txn, uint32_t pgno) { for (int i = 0; i < txn->dirty_count; i++) { if (txn->dirty[i].pgno != pgno) continue; txn->dirty[i] = txn->dirty[txn->dirty_count - 1]; txn->dirty_count--; return 0; } return 0; } /* ── Free a page ───────────────────────────────────────────────────── */ int mneme_page_free(mneme_txn_t *txn, uint32_t pgno) { if (pgno == 0) return 0; mneme_dirty_page_t *dp = dirty_find_entry(txn, pgno); if (dp && dp->origin == MNEME_DIRTY_ALLOCATED) { if (mneme_page_discard_dirty(txn, pgno) < 0) return -1; if ((uint64_t)pgno + 1 == txn->meta.page_count) { txn->meta.page_count--; page_fresh_tail_shrink(txn); return 0; } return page_list_append(&txn->fresh_free_pages, &txn->fresh_free_count, &txn->fresh_free_cap, pgno); } if (dp && dp->origin == MNEME_DIRTY_REUSED) { if (mneme_page_discard_dirty(txn, pgno) < 0) return -1; if (page_list_remove(txn->consumed_reuse_pages, &txn->consumed_reuse_count, pgno) < 0) return -1; return page_list_append(&txn->reuse_pages, &txn->reuse_count, &txn->reuse_cap, pgno); } if (txn->pending_retire_count >= txn->pending_retire_cap) { int new_cap = txn->pending_retire_cap == 0 ? 16 : txn->pending_retire_cap * 2; uint32_t *new_arr = (uint32_t *)realloc(txn->pending_retire, (size_t)new_cap * sizeof(uint32_t)); if (!new_arr) return -1; txn->pending_retire = new_arr; txn->pending_retire_cap = new_cap; } txn->pending_retire[txn->pending_retire_count++] = pgno; return 0; }