ebox/ebox-runtime-index.el

238 lines
9.1 KiB
EmacsLisp

;;; ebox-runtime-index.el --- Immutable runtime indexes for Ebox -*- lexical-binding: t; -*-
;; SPDX-License-Identifier: GPL-3.0-or-later
;;; Commentary:
;; Compressed persistent radix maps used by runtime state. Branches retain
;; only nibble positions where codes diverge; leaves hold exact equal-key
;; collision buckets. Adapters continue to accept legacy hash tables.
;;; Code:
(require 'cl-lib)
(cl-defstruct (ebox-runtime-index
(:constructor ebox-runtime-index--create
(root count key-set-token)))
"Immutable compressed radix index."
root
count
key-set-token)
(defun ebox-runtime-index-empty ()
"Return an empty immutable runtime index."
(ebox-runtime-index--create nil 0 nil))
(defun ebox-runtime-index-like-p (object)
"Return non-nil when OBJECT is a supported runtime index."
(or (ebox-runtime-index-p object) (hash-table-p object)))
(defun ebox-runtime-index--code (key)
"Return a stable 64-bit radix code for KEY."
(logand (if (and (integerp key) (>= key 0)) key (sxhash-equal key))
#xffffffffffffffff))
(defun ebox-runtime-index--branch (shift code &optional original)
"Return a fresh branch for nibble SHIFT and representative CODE.
Copy ORIGINAL when supplied; its metadata already carries SHIFT and CODE.
All branch allocations, including path copies, pass through this function."
(if original
(copy-sequence original)
(let ((branch (make-vector 18 nil)))
(aset branch 0 shift)
(aset branch 1 code)
branch)))
(defun ebox-runtime-index--join (left left-code right right-code)
"Join LEFT and RIGHT above the first differing nibble of their codes.
LEFT-CODE and RIGHT-CODE are distinct 64-bit representative codes."
(let* ((difference (logxor left-code right-code))
(shift (logand (logcount (1- (logand difference (- difference))))
-4))
(branch (ebox-runtime-index--branch shift left-code)))
(aset branch (+ 2 (logand (ash left-code (- shift)) 15)) left)
(aset branch (+ 2 (logand (ash right-code (- shift)) 15)) right)
branch))
(defun ebox-runtime-index--root-get (root key missing)
"Return KEY from persistent ROOT, or MISSING."
(let ((node root)
(code (ebox-runtime-index--code key)))
(while (vectorp node)
(setq node (aref node (+ 2 (logand (ash code (- (aref node 0))) 15)))))
(if (and node (= code (car node)))
(let ((pair (assoc key (cdr node))))
(if pair (cdr pair) missing))
missing)))
(defun ebox-runtime-index-get (key index &optional missing)
"Return KEY's value from INDEX, or MISSING.
INDEX may be an immutable runtime index or a legacy hash table."
(cond
((ebox-runtime-index-p index)
(ebox-runtime-index--root-get (ebox-runtime-index-root index) key missing))
((hash-table-p index) (gethash key index missing))
(t missing)))
(defun ebox-runtime-index-contains-p (key index)
"Return non-nil when INDEX contains KEY, including a nil value."
(let ((missing (make-symbol "ebox-runtime-index-missing")))
(not (eq (ebox-runtime-index-get key index missing) missing))))
(defun ebox-runtime-index--root-put (root key value code)
"Return `(ROOT . ADDED)' after mapping KEY to VALUE using CODE."
(cond
((null root) (cons (cons code (list (cons key value))) t))
((vectorp root)
(let ((shift (aref root 0))
(representative (aref root 1)))
(if (/= (logand (logxor code representative) (1- (ash 1 shift))) 0)
(cons (ebox-runtime-index--join
root representative (cons code (list (cons key value))) code)
t)
(let* ((slot (+ 2 (logand (ash code (- shift)) 15)))
(result (ebox-runtime-index--root-put
(aref root slot) key value code))
(copy (ebox-runtime-index--branch shift representative root)))
(aset copy slot (car result))
(cons copy (cdr result))))))
((= code (car root))
(let* ((bucket (cdr root))
(present (assoc key bucket)))
(cons (cons code
(cons (cons key value)
(if present
(cl-remove key bucket :key #'car :test #'equal)
bucket)))
(not present))))
(t (cons (ebox-runtime-index--join
root (car root) (cons code (list (cons key value))) code)
t))))
(defun ebox-runtime-index-put (key value index)
"Return INDEX with KEY mapped to VALUE.
Immutable indexes return a new value. Legacy hash tables are mutated and
returned for compatibility."
(cond
((ebox-runtime-index-p index)
(let ((result
(ebox-runtime-index--root-put
(ebox-runtime-index-root index) key value
(ebox-runtime-index--code key))))
(ebox-runtime-index--create
(car result)
(+ (ebox-runtime-index-count index) (if (cdr result) 1 0))
(if (cdr result)
(make-symbol "ebox-runtime-index-key-set")
(ebox-runtime-index-key-set-token index)))))
((hash-table-p index) (puthash key value index) index)
(t (signal 'wrong-type-argument (list 'ebox-runtime-index-like-p index)))))
(defun ebox-runtime-index--root-delete (root key code)
"Return `(ROOT . REMOVED)' after deleting KEY using CODE."
(cond
((null root) (cons nil nil))
((vectorp root)
(let* ((shift (aref root 0))
(slot (+ 2 (logand (ash code (- shift)) 15)))
(result (ebox-runtime-index--root-delete (aref root slot) key code)))
(if (not (cdr result))
(cons root nil)
(let ((children 0) survivor)
(unless (car result)
(dotimes (offset 16)
(let ((position (+ offset 2)))
(when (and (/= position slot) (aref root position))
(cl-incf children)
(setq survivor (aref root position))))))
(if (= children 1)
(cons survivor t)
(let ((copy (ebox-runtime-index--branch shift (aref root 1) root)))
(aset copy slot (car result))
(cons copy t)))))))
((and (= code (car root)) (assoc key (cdr root)))
(let ((bucket (cl-remove key (cdr root) :key #'car :test #'equal)))
(cons (and bucket (cons code bucket)) t)))
(t (cons root nil))))
(defun ebox-runtime-index-delete (key index)
"Return INDEX without KEY.
Immutable indexes return a new value. Legacy hash tables are mutated and
returned for compatibility."
(cond
((ebox-runtime-index-p index)
(let ((result
(ebox-runtime-index--root-delete
(ebox-runtime-index-root index) key
(ebox-runtime-index--code key))))
(if (cdr result)
(let ((count (1- (ebox-runtime-index-count index))))
(ebox-runtime-index--create
(car result) count
(and (> count 0)
(make-symbol "ebox-runtime-index-key-set"))))
index)))
((hash-table-p index) (remhash key index) index)
(t (signal 'wrong-type-argument (list 'ebox-runtime-index-like-p index)))))
(defun ebox-runtime-index-size (index)
"Return the number of entries in INDEX."
(cond
((ebox-runtime-index-p index) (ebox-runtime-index-count index))
((hash-table-p index) (hash-table-count index))
(t 0)))
(defun ebox-runtime-index-key-set-equal-p (left right)
"Return non-nil when supported indexes LEFT and RIGHT have equal keys."
(let ((persistent-shared-keys-p
(and (ebox-runtime-index-p left)
(ebox-runtime-index-p right)
(= (ebox-runtime-index-count left)
(ebox-runtime-index-count right))
(eq (ebox-runtime-index-key-set-token left)
(ebox-runtime-index-key-set-token right)))))
(cond
(persistent-shared-keys-p t)
((and (ebox-runtime-index-like-p left)
(ebox-runtime-index-like-p right)
(= (ebox-runtime-index-size left) (ebox-runtime-index-size right)))
(let ((equal-p t)
(missing (make-symbol "ebox-runtime-index-missing")))
(ebox-runtime-index-map
(lambda (key _value)
(when (eq (ebox-runtime-index-get key right missing) missing)
(setq equal-p nil)))
left)
equal-p))
(t nil))))
(defun ebox-runtime-index--map-root (function root)
"Call FUNCTION for entries below ROOT, visiting only live trie nodes."
(when root
(if (vectorp root)
(dotimes (branch 16)
(let ((child (aref root (+ 2 branch))))
(when child (ebox-runtime-index--map-root function child))))
(dolist (pair (cdr root)) (funcall function (car pair) (cdr pair))))))
(defun ebox-runtime-index-map (function index)
"Call FUNCTION with every key and value in INDEX."
(cond
((ebox-runtime-index-p index)
(ebox-runtime-index--map-root
function (ebox-runtime-index-root index)))
((hash-table-p index) (maphash function index))
(t (signal 'wrong-type-argument (list 'ebox-runtime-index-like-p index))))
nil)
(defun ebox-runtime-index-keys (index)
"Return a list of keys in INDEX."
(let (keys)
(ebox-runtime-index-map (lambda (key _value) (push key keys)) index)
keys))
(provide 'ebox-runtime-index)
;;; ebox-runtime-index.el ends here