238 lines
9.1 KiB
EmacsLisp
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
|