tp/tp-reactive.el

1684 lines
75 KiB
EmacsLisp

;;; tp-reactive.el --- Exact signals and binding scheduler -*- lexical-binding: t -*-
;; Copyright (C) 2024-2026 Geekinney
;; Author: Geekinney (kinneyzhang666@gmail.com)
;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation; either version 3 of
;; the License, or (at your option) any later version.
;;; Commentary:
;; TP 2.0's exact signal-to-binding dependency graph, transaction-local
;; scheduler, scoped variable adapters, and rollback state.
;;; Code:
(require 'cl-lib)
(require 'tp-core)
(require 'tp-transaction)
(define-error 'tp-reactive-error "TP reactive runtime error")
(define-error 'tp-invalid-signal-scope "Invalid TP signal scope"
'tp-reactive-error)
(define-error 'tp-disposed-signal "TP signal is disposed" 'tp-reactive-error)
(define-error 'tp-disposed-binding "TP binding is disposed" 'tp-reactive-error)
(define-error 'tp-binding-cycle "TP binding dependency cycle"
'tp-reactive-error)
(cl-defstruct (tp-signal (:constructor tp--make-signal))
"Reactive source with a committed value and exact subscribers."
id committed-value equality subscribers scope adapter-key disposed revision)
(cl-defstruct (tp-binding (:constructor tp--make-binding))
"Memoized computation installed on one owner and namespaced key."
id owner key compute equality last-value initialized-p dependencies
subscribers dirty state revision lifecycle disposed)
(cl-defstruct (tp--binding-snapshot
(:constructor tp--make-binding-snapshot))
compute equality last-value initialized-p dependencies dirty state
revision lifecycle disposed)
(cl-defstruct (tp--transaction-participant
(:constructor tp--make-transaction-participant))
"One structured transaction participant."
key rollback protocol order stage precommit after-commit journal state)
(cl-defstruct (tp--signal-commit-entry
(:constructor tp--make-signal-commit-entry))
"One transaction signal's candidate and exact committed rollback state."
signal old-committed-value old-revision candidate-value)
(defconst tp--reactive-absent (make-symbol "tp-reactive-absent"))
(defvar tp--signal-id-counter 0)
(defvar tp--binding-id-counter 0)
(defvar tp--signals nil)
(defvar tp--bindings nil)
(defvar tp--owner-bindings (make-hash-table :test #'eq))
(defvar tp--variable-signals (make-hash-table :test #'equal))
(defvar tp--variable-signal-watched nil)
(defvar-local tp--buffer-signals nil)
(defvar tp--reactive-counters
(list :invalidated 0 :recomputed 0 :skipped 0
:subscription-added 0 :subscription-removed 0))
(defvar tp--transaction-active nil)
;;;###autoload
(defun tp-transaction-active-p ()
"Return non-nil while the current dynamic extent is in a TP transaction."
(and tp--transaction-active t))
(defvar tp--transaction-signal-values nil)
(defvar tp--transaction-signals nil)
(defvar tp--transaction-dirty-set nil)
(defvar tp--transaction-dirty-queue nil)
(defvar tp--transaction-binding-snapshots nil)
(defvar tp--transaction-created-bindings nil)
(defvar tp--transaction-recompute-counts nil)
(defvar tp--transaction-extensions nil)
(defvar tp--transaction-counter-start nil)
(defvar tp--transaction-after-commit-callbacks nil)
(defvar tp--transaction-participants nil)
(defvar tp--transaction-participant-keys nil)
(defvar tp--transaction-participant-order 0)
(defvar tp--transaction-published-participants nil)
(defvar tp--transaction-signal-commit-journal nil)
(defvar tp--transaction-id nil)
(defvar tp--transaction-phase nil)
(defvar tp--transaction-phase-start nil)
(defvar tp--transaction-phase-timings nil)
(defvar tp--transaction-publication-batch nil)
(defvar tp--transaction-structured-participants nil)
(defvar tp--transaction-final-marker-registry nil)
(defvar tp--transaction-final-marker-owner-keys nil)
(defvar tp--transaction-final-marker-count 0)
(defvar tp--transaction-final-marker-slot-writes 0)
(defvar tp--transaction-final-markers-frozen-p nil)
(defvar tp--transaction-applied-final-marker-count 0)
(defvar tp--transaction-marker-restore-failures nil)
(defvar tp--transaction-outcome nil)
(defvar tp--transaction-outcome-cell nil)
(defvar tp--transaction-contained-failures nil)
(defvar tp--last-transaction-diagnostics nil)
(defvar tp--last-transaction-outcome nil)
(defvar tp--last-shadow-proof nil)
(defvar tp--current-binding nil)
(defvar tp--binding-compute-stack nil)
(defvar tp--collected-dependency-set nil)
(defvar tp--collected-dependencies nil)
(defvar tp--binding-touch-function nil)
(defvar tp--binding-changed-functions nil)
(defvar tp--transaction-publish-functions nil)
(defvar tp--transaction-precommit-functions nil
"Internal TP-owned precommit hook symbols in deterministic order.")
(defconst tp--transaction-precommit-allowed-functions
'(tp--surface-precommit-transaction)
"Exact TP-owned symbols allowed in the internal precommit registry.")
(defvar tp--transaction-final-accept-function
#'tp--transaction-noop-final-accept
"Internal single final-accept function for the active transaction.")
(defvar tp--transaction-rollback-functions nil)
(defvar tp--transaction-rollback-final-functions nil)
(defvar tp--transaction-committed-functions nil)
(defvar tp--transaction-participant-precommit-allowed-functions nil
"Declared internal structured-participant precommit validators.")
(defconst tp--transaction-condition-trailer-tag
(make-symbol "tp--transaction-condition-trailer")
"Unforgeable tag separating primary condition data from TP metadata.")
(defun tp--counter-increment (key)
"Increment reactive counter KEY."
(setq tp--reactive-counters
(plist-put tp--reactive-counters key
(1+ (plist-get tp--reactive-counters key)))))
(defun tp-reactive-counters ()
"Return public signal and binding scheduler counters."
(copy-sequence tp--reactive-counters))
(defun tp-reactive-reset-counters ()
"Reset public signal and binding scheduler counters."
(setq tp--reactive-counters
(list :invalidated 0 :recomputed 0 :skipped 0
:subscription-added 0 :subscription-removed 0)))
(defun tp--dependency-subscribers (dependency)
"Return the subscriber table owned by DEPENDENCY."
(cond ((tp-signal-p dependency) (tp-signal-subscribers dependency))
((tp-binding-p dependency) (tp-binding-subscribers dependency))
(t (signal 'wrong-type-argument
(list '(or tp-signal-p tp-binding-p) dependency)))))
(defun tp--subscription-add (dependency binding)
"Subscribe BINDING to DEPENDENCY."
(let ((subscribers (tp--dependency-subscribers dependency)))
(unless (gethash binding subscribers)
(puthash binding t subscribers)
(tp--counter-increment :subscription-added))))
(defun tp--subscription-remove (dependency binding)
"Unsubscribe BINDING from DEPENDENCY."
(let ((subscribers (tp--dependency-subscribers dependency)))
(when (gethash binding subscribers)
(remhash binding subscribers)
(tp--counter-increment :subscription-removed))))
(defun tp--sorted-subscribers (dependency)
"Return DEPENDENCY subscribers ordered by stable binding id."
(let (bindings)
(maphash (lambda (binding _present) (push binding bindings))
(tp--dependency-subscribers dependency))
(sort bindings (lambda (left right)
(< (tp-binding-id left) (tp-binding-id right))))))
(defun tp--binding-snapshot (binding)
"Return a rollback snapshot of BINDING."
(tp--make-binding-snapshot
:compute (tp-binding-compute binding)
:equality (tp-binding-equality binding)
:last-value (tp-binding-last-value binding)
:initialized-p (tp-binding-initialized-p binding)
:dependencies (copy-sequence (tp-binding-dependencies binding))
:dirty (tp-binding-dirty binding)
:state (tp-binding-state binding)
:revision (tp-binding-revision binding)
:lifecycle (tp-binding-lifecycle binding)
:disposed (tp-binding-disposed binding)))
(defun tp--snapshot-binding (binding)
"Save BINDING once for the active transaction."
(when (and tp--transaction-active
(not (memq binding tp--transaction-created-bindings))
(not (gethash binding tp--transaction-binding-snapshots)))
(puthash binding (tp--binding-snapshot binding)
tp--transaction-binding-snapshots)))
(defun tp--owner-binding-table (owner &optional create)
"Return OWNER's binding table, creating it when CREATE is non-nil."
(or (gethash owner tp--owner-bindings)
(when create
(let ((table (make-hash-table :test #'equal)))
(puthash owner table tp--owner-bindings)
table))))
(defun tp--register-binding (binding)
"Register BINDING under its owner and key."
(puthash (tp-binding-key binding) binding
(tp--owner-binding-table (tp-binding-owner binding) t))
(cl-pushnew binding tp--bindings :test #'eq))
(defun tp--unregister-binding (binding)
"Remove BINDING from owner and global registries."
(when-let* ((table (tp--owner-binding-table (tp-binding-owner binding))))
(remhash (tp-binding-key binding) table)
(when (zerop (hash-table-count table))
(remhash (tp-binding-owner binding) tp--owner-bindings)))
(setq tp--bindings (delq binding tp--bindings)))
(defun tp--enqueue-binding (binding)
"Add dirty BINDING to the current transaction queue once."
(unless (gethash binding tp--transaction-dirty-set)
(puthash binding t tp--transaction-dirty-set)
(push binding tp--transaction-dirty-queue)))
(defun tp--mark-binding-dirty (binding)
"Mark live BINDING dirty in the current transaction."
(when (and (tp-binding-p binding) (not (tp-binding-disposed binding)))
(tp--snapshot-binding binding)
(unless (tp-binding-dirty binding)
(setf (tp-binding-dirty binding) t)
(tp--counter-increment :invalidated))
(tp--enqueue-binding binding)))
(defun tp--invalidate-subscribers (dependency &optional skip)
"Dirty DEPENDENCY subscribers except SKIP."
(dolist (binding (tp--sorted-subscribers dependency))
(unless (eq binding skip)
(tp--mark-binding-dirty binding))))
(defun tp--record-dependency (dependency)
"Record DEPENDENCY for the binding currently being computed."
(when tp--current-binding
(unless (gethash dependency tp--collected-dependency-set)
(puthash dependency t tp--collected-dependency-set)
(push dependency tp--collected-dependencies))))
(defun tp--signal-value-in-transaction (signal)
"Return SIGNAL's candidate or committed value."
(if tp--transaction-active
(let ((candidate (gethash signal tp--transaction-signal-values
tp--reactive-absent)))
(if (eq candidate tp--reactive-absent)
(tp-signal-committed-value signal)
candidate))
(tp-signal-committed-value signal)))
(defun tp--validate-live-signal (signal)
"Signal an error unless SIGNAL is live."
(unless (tp-signal-p signal)
(signal 'wrong-type-argument (list 'tp-signal-p signal)))
(when (tp-signal-disposed signal)
(signal 'tp-disposed-signal (list (tp-signal-id signal)))))
(defun tp-signal-live-p (signal)
"Return non-nil when SIGNAL is a live TP signal."
(and (tp-signal-p signal) (not (tp-signal-disposed signal))))
(defun tp--buffer-signal-kill ()
"Dispose signals scoped to the buffer being killed."
(let ((signals tp--buffer-signals))
(setq tp--buffer-signals nil)
(dolist (signal signals)
(tp--dispose-signal signal))))
(cl-defun tp-signal-create (initial-value &key (equality #'equal) (scope 'global))
"Create a signal holding INITIAL-VALUE.
EQUALITY compares writes. SCOPE is `global' or a live buffer."
(unless (functionp equality)
(signal 'wrong-type-argument (list 'functionp equality)))
(unless (or (eq scope 'global) (buffer-live-p scope))
(signal 'tp-invalid-signal-scope (list scope)))
(let ((signal (tp--make-signal
:id (cl-incf tp--signal-id-counter)
:committed-value initial-value :equality equality
:subscribers (make-hash-table :test #'eq)
:scope scope :revision 0)))
(push signal tp--signals)
(when (bufferp scope)
(with-current-buffer scope
(push signal tp--buffer-signals)
(add-hook 'kill-buffer-hook #'tp--buffer-signal-kill nil t)))
signal))
(defun tp-signal-peek (signal)
"Return SIGNAL's effective value without collecting a dependency."
(tp--validate-live-signal signal)
(tp--signal-value-in-transaction signal))
(defun tp-signal-read (signal)
"Read SIGNAL and register the current binding as a subscriber."
(tp--validate-live-signal signal)
(tp--record-dependency signal)
(tp--signal-value-in-transaction signal))
(defun tp--set-signal-candidate (signal value)
"Set candidate VALUE for SIGNAL in the active transaction."
(let ((old (tp--signal-value-in-transaction signal)))
(unless (funcall (tp-signal-equality signal) old value)
(when (eq (gethash signal tp--transaction-signal-values
tp--reactive-absent)
tp--reactive-absent)
(push signal tp--transaction-signals))
(puthash signal value tp--transaction-signal-values)
(tp--invalidate-subscribers signal)
(when (and tp--current-binding
(gethash signal tp--collected-dependency-set))
(tp--mark-binding-dirty tp--current-binding))))
value)
(defun tp-signal-set (signal value)
"Set SIGNAL to VALUE transactionally and return VALUE."
(tp--validate-live-signal signal)
(if tp--transaction-active
(tp--set-signal-candidate signal value)
(tp--call-with-transaction
(lambda () (tp--set-signal-candidate signal value)))))
(defun tp-signal-subscriber-count (signal)
"Return the number of subscribers attached to SIGNAL.
Disposed signals return zero."
(unless (tp-signal-p signal)
(signal 'wrong-type-argument (list 'tp-signal-p signal)))
(hash-table-count (tp-signal-subscribers signal)))
(defun tp--binding-dependency-path (start target visited)
"Return a binding path from START to TARGET, avoiding VISITED."
(cond
((eq start target) (list target))
((memq start visited) nil)
(t
(cl-loop for dependency in (tp-binding-dependencies start)
when (tp-binding-p dependency)
for path = (tp--binding-dependency-path
dependency target (cons start visited))
when path return (cons start path)))))
(defun tp--stack-cycle-path (target)
"Return the active compute-stack cycle ending at TARGET."
(let ((path (reverse tp--binding-compute-stack)))
(while (and path (not (eq (car path) target)))
(setq path (cdr path)))
(append path (list target))))
(defun tp--signal-binding-cycle (path)
"Signal a binding-cycle error for binding PATH."
(signal 'tp-binding-cycle
(list (mapcar (lambda (binding)
(tp--copy-property-value
(tp-binding-key binding)))
path))))
(defun tp--validate-binding-read (binding &optional computing-only)
"Validate reading BINDING from the current computation.
When COMPUTING-ONLY is non-nil, defer old-graph traversal until after a
dirty target has recomputed."
(when tp--current-binding
(when (eq (tp-binding-state binding) 'computing)
(tp--signal-binding-cycle (tp--stack-cycle-path binding)))
(unless computing-only
(when-let* ((path (tp--binding-dependency-path
binding tp--current-binding nil)))
(tp--signal-binding-cycle (cons tp--current-binding path))))))
(defun tp--replace-binding-dependencies (binding dependencies)
"Replace BINDING dependencies with DEPENDENCIES."
(let ((old (tp-binding-dependencies binding)))
(dolist (dependency old)
(unless (memq dependency dependencies)
(tp--subscription-remove dependency binding)))
(dolist (dependency dependencies)
(unless (memq dependency old)
(tp--subscription-add dependency binding)))
(setf (tp-binding-dependencies binding) dependencies)))
(defun tp--note-binding-recompute (binding)
"Record one recomputation of BINDING and reject runaway feedback."
(let ((count (1+ (gethash binding tp--transaction-recompute-counts 0))))
(puthash binding count tp--transaction-recompute-counts)
(when (> count 100)
(tp--signal-binding-cycle (list binding binding)))))
(defun tp--binding-recompute (binding)
"Recompute dirty BINDING inside the active transaction."
(tp--snapshot-binding binding)
(tp--note-binding-recompute binding)
(let ((requester tp--current-binding)
(old (tp-binding-last-value binding))
(initialized (tp-binding-initialized-p binding))
(tp--current-binding binding)
(tp--binding-compute-stack (cons binding tp--binding-compute-stack))
(tp--collected-dependency-set (make-hash-table :test #'eq))
(tp--collected-dependencies nil))
(setf (tp-binding-state binding) 'computing
(tp-binding-dirty binding) nil)
(let* ((value (funcall (tp-binding-compute binding)))
(dependencies (nreverse tp--collected-dependencies))
(changed (or (not initialized)
(not (funcall (tp-binding-equality binding)
old value)))))
(tp--replace-binding-dependencies binding dependencies)
(setf (tp-binding-state binding) 'clean)
(tp--counter-increment :recomputed)
(if changed
(progn
(setf (tp-binding-last-value binding) value
(tp-binding-initialized-p binding) t
(tp-binding-revision binding)
(1+ (tp-binding-revision binding)))
(run-hook-with-args 'tp--binding-changed-functions
binding old value)
(tp--invalidate-subscribers binding requester))
(tp--counter-increment :skipped))
value)))
(defun tp--validate-live-binding (binding)
"Signal an error unless BINDING is live."
(unless (tp-binding-p binding)
(signal 'wrong-type-argument (list 'tp-binding-p binding)))
(when (tp-binding-disposed binding)
(signal 'tp-disposed-binding (list (tp-binding-id binding)))))
(defun tp-binding-live-p (binding)
"Return non-nil when BINDING is a live TP binding."
(and (tp-binding-p binding) (not (tp-binding-disposed binding))))
(defun tp-binding-read (binding)
"Read BINDING's memoized value and collect a dependency."
(tp--validate-live-binding binding)
(tp--validate-binding-read binding t)
(when (tp-binding-dirty binding)
(if tp--transaction-active
(tp--binding-recompute binding)
(tp--call-with-transaction (lambda () (tp--binding-recompute binding)))))
(tp--validate-binding-read binding)
(tp--record-dependency binding)
(tp-binding-last-value binding))
(defun tp-binding-subscriber-count (binding)
"Return the number of bindings depending directly on BINDING.
Disposed bindings return zero."
(unless (tp-binding-p binding)
(signal 'wrong-type-argument (list 'tp-binding-p binding)))
(hash-table-count (tp-binding-subscribers binding)))
(defun tp-binding-dependency-count (binding)
"Return BINDING's current direct dependency count.
Disposed bindings return zero."
(unless (tp-binding-p binding)
(signal 'wrong-type-argument (list 'tp-binding-p binding)))
(length (tp-binding-dependencies binding)))
(defun tp--validate-binding-options (compute equality lifecycle)
"Validate binding COMPUTE, EQUALITY, and LIFECYCLE."
(unless (functionp compute)
(signal 'wrong-type-argument (list 'functionp compute)))
(unless (functionp equality)
(signal 'wrong-type-argument (list 'functionp equality)))
(unless (memq lifecycle '(delete retain))
(signal 'tp-reactive-error (list :lifecycle lifecycle))))
(defun tp--bind-in-transaction (owner key compute equality lifecycle)
"Install COMPUTE for OWNER and KEY in the active transaction.
EQUALITY compares values and LIFECYCLE controls retention."
(let* ((table (tp--owner-binding-table owner t))
(binding (gethash key table)))
(if binding
(unless (and (eq compute (tp-binding-compute binding))
(eq equality (tp-binding-equality binding))
(eq lifecycle (tp-binding-lifecycle binding)))
(tp--snapshot-binding binding)
(setf (tp-binding-compute binding) compute
(tp-binding-equality binding) equality
(tp-binding-lifecycle binding) lifecycle)
(tp--mark-binding-dirty binding))
(setq binding
(tp--make-binding
:id (cl-incf tp--binding-id-counter)
:owner owner :key (tp--copy-property-value key) :compute compute
:equality equality :subscribers (make-hash-table :test #'eq)
:dirty t :state 'clean :revision 0 :lifecycle lifecycle))
(tp--register-binding binding)
(push binding tp--transaction-created-bindings)
(tp--enqueue-binding binding))
(when tp--binding-touch-function
(funcall tp--binding-touch-function binding))
binding))
(cl-defun tp-bind (owner key compute &key (equality #'equal) (lifecycle 'delete))
"Idempotently install COMPUTE on OWNER under namespaced KEY.
EQUALITY suppresses unchanged downstream updates. LIFECYCLE is `delete'
or `retain'."
(when (null owner)
(signal 'tp-reactive-error (list :owner owner)))
(when (null key)
(signal 'tp-reactive-error (list :binding-key key)))
(tp--validate-binding-options compute equality lifecycle)
(if tp--transaction-active
(tp--bind-in-transaction owner key compute equality lifecycle)
(tp--call-with-transaction
(lambda () (tp--bind-in-transaction
owner key compute equality lifecycle)))))
(defun tp--bind-precomputed-in-transaction
(owner key compute value dependencies equality lifecycle)
"Install OWNER's KEY using COMPUTE, VALUE, and explicit DEPENDENCIES.
EQUALITY controls change detection and LIFECYCLE controls candidate omission."
(let ((table (tp--owner-binding-table owner t)))
(when (gethash key table)
(signal 'tp-reactive-error (list :precomputed-binding-exists key)))
(dolist (dependency dependencies)
(cond ((tp-signal-p dependency) (tp--validate-live-signal dependency))
((tp-binding-p dependency) (tp--validate-live-binding dependency))
(t (signal 'wrong-type-argument
(list '(or tp-signal-p tp-binding-p) dependency)))))
(let ((binding
(tp--make-binding
:id (cl-incf tp--binding-id-counter)
:owner owner :key (tp--copy-property-value key) :compute compute
:equality equality :last-value value :initialized-p t
:dependencies (copy-sequence dependencies)
:subscribers (make-hash-table :test #'eq)
:dirty nil :state 'clean :revision 1 :lifecycle lifecycle)))
(tp--register-binding binding)
(push binding tp--transaction-created-bindings)
(dolist (dependency dependencies)
(tp--subscription-add dependency binding))
(when tp--binding-touch-function
(funcall tp--binding-touch-function binding))
binding)))
(cl-defun tp-bind-precomputed
(owner key compute value dependencies
&key (equality #'equal) (lifecycle 'delete))
"Install a new binding with precomputed VALUE and explicit DEPENDENCIES.
OWNER and KEY identify the binding; EQUALITY and LIFECYCLE retain their normal
`tp-bind' meanings.
COMPUTE remains the authoritative recomputation function after any dependency
changes. This entry avoids evaluating COMPUTE merely to rediscover a value
and graph edges already produced by a compiler or pure projection pass."
(when (null owner)
(signal 'tp-reactive-error (list :owner owner)))
(when (null key)
(signal 'tp-reactive-error (list :binding-key key)))
(tp--validate-binding-options compute equality lifecycle)
(unless (proper-list-p dependencies)
(signal 'wrong-type-argument (list 'proper-list-p dependencies)))
(if tp--transaction-active
(tp--bind-precomputed-in-transaction
owner key compute value dependencies equality lifecycle)
(tp--call-with-transaction
(lambda ()
(tp--bind-precomputed-in-transaction
owner key compute value dependencies equality lifecycle)))))
(defun tp--detach-binding-dependencies (binding)
"Remove BINDING from all dependency subscriber tables."
(dolist (dependency (tp-binding-dependencies binding))
(tp--subscription-remove dependency binding))
(setf (tp-binding-dependencies binding) nil))
(defun tp--detach-binding-subscribers (binding)
"Detach every direct subscriber from BINDING."
(dolist (subscriber (tp--sorted-subscribers binding))
(tp--snapshot-binding subscriber)
(setf (tp-binding-dependencies subscriber)
(delq binding (tp-binding-dependencies subscriber))
(tp-binding-dirty subscriber) t)
(tp--subscription-remove binding subscriber)))
(defun tp--dispose-binding (binding)
"Dispose live BINDING without starting a transaction."
(unless (tp-binding-disposed binding)
(tp--snapshot-binding binding)
(tp--detach-binding-dependencies binding)
(tp--detach-binding-subscribers binding)
(tp--unregister-binding binding)
(setf (tp-binding-disposed binding) t
(tp-binding-dirty binding) nil
(tp-binding-state binding) 'disposed)))
(defun tp-binding-dispose-owner (owner)
"Dispose every binding installed on OWNER and return the count."
(let ((bindings (when-let* ((table (tp--owner-binding-table owner)))
(let (items)
(maphash (lambda (_key binding) (push binding items)) table)
(sort items (lambda (left right)
(< (tp-binding-id left)
(tp-binding-id right))))))))
(if tp--transaction-active
(dolist (binding bindings) (tp--dispose-binding binding))
(tp--call-with-transaction
(lambda () (dolist (binding bindings) (tp--dispose-binding binding)))))
(length bindings)))
(defun tp-binding-owner-bindings (owner)
"Return OWNER's live bindings ordered by stable binding id."
(when-let* ((table (tp--owner-binding-table owner)))
(let (bindings)
(maphash (lambda (_key binding) (push binding bindings)) table)
(sort bindings (lambda (left right)
(< (tp-binding-id left) (tp-binding-id right)))))))
(defun tp-binding-dispose (binding)
"Dispose BINDING and detach all of its graph edges."
(tp--validate-live-binding binding)
(if tp--transaction-active
(tp--dispose-binding binding)
(tp--call-with-transaction (lambda () (tp--dispose-binding binding))))
nil)
(defun tp--transaction-extension (key &optional create)
"Return transaction extension state for KEY.
When CREATE is non-nil, install and return a fresh hash table when absent."
(unless tp--transaction-active
(signal 'tp-reactive-error (list :outside-transaction key)))
(or (gethash key tp--transaction-extensions)
(when create
(let ((state (make-hash-table :test #'eq)))
(puthash key state tp--transaction-extensions)
state))))
(defun tp--transaction-counter-delta (key)
"Return active transaction counter delta for KEY."
(if tp--transaction-counter-start
(- (plist-get tp--reactive-counters key)
(plist-get tp--transaction-counter-start key))
0))
(defun tp--enqueue-after-commit (function)
"Run FUNCTION after the active outer transaction has exited."
(unless tp--transaction-active
(signal 'tp-reactive-error (list :outside-transaction function)))
(push function tp--transaction-after-commit-callbacks))
(defun tp--transaction-participant-precommit-function-p (function)
"Return non-nil when FUNCTION is a declared internal participant validator."
(or (null function)
(and (symbolp function)
(memq function
tp--transaction-participant-precommit-allowed-functions)
(fboundp function))))
(defun tp--transaction-register-participant (participant)
"Register structured PARTICIPANT once in the active transaction."
(unless tp--transaction-active
(signal 'tp-reactive-error
(list :participant-outside-transaction
(tp--transaction-participant-key participant))))
(let ((key (tp--transaction-participant-key participant)))
(unless key
(signal 'tp-reactive-error (list :participant-key key)))
(when (member key tp--transaction-participant-keys)
(signal 'tp-reactive-error (list :duplicate-participant-key key)))
(push (tp--copy-property-value key) tp--transaction-participant-keys)
(push participant tp--transaction-participants)
participant))
(defun tp--transaction-make-participant
(key stage rollback protocol precommit after-commit journal)
"Build a participant from KEY, STAGE, ROLLBACK, and PROTOCOL.
PRECOMMIT and AFTER-COMMIT are optional internal callbacks; JOURNAL is opaque
owner-local rollback state."
(unless (functionp stage)
(signal 'wrong-type-argument (list 'functionp stage)))
(unless (functionp rollback)
(signal 'wrong-type-argument (list 'functionp rollback)))
(unless (tp--transaction-participant-precommit-function-p precommit)
(signal 'tp-reactive-error
(list :invalid-participant-precommit precommit)))
(unless (or (null after-commit) (functionp after-commit))
(signal 'wrong-type-argument (list 'functionp after-commit)))
(tp--make-transaction-participant
:key (tp--copy-property-value key)
:rollback rollback
:protocol protocol
:order (prog1 tp--transaction-participant-order
(cl-incf tp--transaction-participant-order))
:stage stage
:precommit precommit
:after-commit after-commit
:journal journal
:state 'prepared))
(cl-defun tp--transaction-participate-v2
(&key key stage rollback precommit after-commit journal)
"Register internal KEY with structured STAGE and ROLLBACK capabilities.
PRECOMMIT and AFTER-COMMIT are optional declared callbacks. JOURNAL is the
participant's opaque owner-local state."
(unless tp--transaction-active
(signal 'tp-reactive-error (list :participant-outside-transaction key)))
(unless key
(signal 'tp-reactive-error (list :participant-key key)))
(tp--transaction-register-participant
(tp--transaction-make-participant
key stage rollback 'v2 precommit after-commit journal)))
;;;###autoload
(cl-defun tp-transaction-participate-v2
(&key key stage rollback precommit after-commit journal)
"Register a structured rollback-capable participant under KEY.
STAGE and ROLLBACK are required no-argument functions. PRECOMMIT may be one
declared package-owned validator; AFTER-COMMIT is contained work queued only
after final accept. JOURNAL is opaque owner-local rollback state. Return KEY
without exposing TP's internal participant object."
(tp--transaction-participate-v2
:key key :stage stage :rollback rollback :precommit precommit
:after-commit after-commit :journal journal)
key)
(defun tp--transaction-participants-in-registration-order ()
"Return the authoritative participants in deterministic declaration order."
(reverse tp--transaction-participants))
(defun tp--transaction-validate-structured-participants ()
"Return the frozen participant vector after exact identity validation."
(let* ((participants tp--transaction-structured-participants)
(registered (tp--transaction-participants-in-registration-order))
(count (length registered)))
(unless (and (vectorp participants) (= (length participants) count))
(signal 'tp-publication-binding-error
(list :participant-count participants registered)))
(cl-loop for participant in registered
for index from 0
unless (eq participant (aref participants index))
do (signal 'tp-publication-binding-error
(list :participant-order index participant
(aref participants index))))
(when tp--transaction-publication-batch
(let ((candidate-participants
(tp-publication-batch-candidate-participants
tp--transaction-publication-batch)))
(unless (eq candidate-participants participants)
(signal 'tp-publication-binding-error
(list :participant-vector candidate-participants
participants)))))
participants))
(defun tp--stage-structured-transaction-participants ()
"Stage the frozen structured participant vector in declaration order."
(let ((participants (tp--transaction-validate-structured-participants)))
(dotimes (index (length participants))
(let ((participant (aref participants index)))
(push participant tp--transaction-published-participants)
(setf (tp--transaction-participant-state participant) 'staged)
(funcall (tp--transaction-participant-stage participant))))))
(defun tp--rollback-transaction-participants ()
"Rollback published participants and return any failures."
(let (failures)
(dolist (participant tp--transaction-published-participants)
(unwind-protect
(condition-case failure
(funcall (tp--transaction-participant-rollback participant))
((error quit)
(push (list 'participants
(tp--transaction-participant-key participant)
failure)
failures)))
(setf (tp--transaction-participant-state participant) 'rolled-back)))
(dolist (participant tp--transaction-participants)
(when (eq (tp--transaction-participant-state participant) 'prepared)
(setf (tp--transaction-participant-state participant) 'rolled-back)))
(nreverse failures)))
(defun tp--run-structured-transaction-participant-precommits ()
"Run validators from the frozen structured participant vector."
(let ((participants (tp--transaction-validate-structured-participants)))
(dotimes (index (length participants))
(when-let* ((function
(tp--transaction-participant-precommit
(aref participants index))))
(unless (tp--transaction-participant-precommit-function-p function)
(signal 'tp-reactive-error
(list :invalid-participant-precommit function)))
(funcall function)))))
(defun tp--commit-structured-transaction-participants ()
"Commit states from the frozen structured participant vector."
(let ((participants (tp--transaction-validate-structured-participants)))
(dotimes (index (length participants))
(let ((participant (aref participants index)))
(when (eq (tp--transaction-participant-state participant) 'staged)
(setf (tp--transaction-participant-state participant) 'committed)
(when-let* ((function
(tp--transaction-participant-after-commit participant)))
(tp--enqueue-after-commit function)))))))
(defun tp--dequeue-dirty-binding ()
"Return and remove the next queued dirty binding."
(let (binding)
(while (and tp--transaction-dirty-queue (null binding))
(let ((candidate (pop tp--transaction-dirty-queue)))
(when (gethash candidate tp--transaction-dirty-set)
(remhash candidate tp--transaction-dirty-set)
(setq binding candidate))))
binding))
(defun tp--flush-dirty-bindings ()
"Recompute the transaction's exact dirty closure."
(let (binding)
(while (setq binding (tp--dequeue-dirty-binding))
(when (and (tp-binding-live-p binding) (tp-binding-dirty binding))
(tp--binding-recompute binding)))))
(defun tp--restore-binding (binding snapshot)
"Restore BINDING fields from SNAPSHOT."
(setf (tp-binding-compute binding) (tp--binding-snapshot-compute snapshot)
(tp-binding-equality binding) (tp--binding-snapshot-equality snapshot)
(tp-binding-last-value binding) (tp--binding-snapshot-last-value snapshot)
(tp-binding-initialized-p binding)
(tp--binding-snapshot-initialized-p snapshot)
(tp-binding-dependencies binding)
(copy-sequence (tp--binding-snapshot-dependencies snapshot))
(tp-binding-dirty binding) (tp--binding-snapshot-dirty snapshot)
(tp-binding-state binding) (tp--binding-snapshot-state snapshot)
(tp-binding-revision binding) (tp--binding-snapshot-revision snapshot)
(tp-binding-lifecycle binding) (tp--binding-snapshot-lifecycle snapshot)
(tp-binding-disposed binding) (tp--binding-snapshot-disposed snapshot)))
(defun tp--rollback-bindings ()
"Restore all bindings touched by the active transaction."
(let (snapshots)
(maphash (lambda (binding snapshot)
(push (cons binding snapshot) snapshots))
tp--transaction-binding-snapshots)
(dolist (binding (append tp--transaction-created-bindings
(mapcar #'car snapshots)))
(tp--detach-binding-dependencies binding))
(dolist (binding tp--transaction-created-bindings)
(tp--unregister-binding binding)
(setf (tp-binding-disposed binding) t
(tp-binding-state binding) 'disposed))
(dolist (entry snapshots)
(pcase-let ((`(,binding . ,snapshot) entry))
(tp--restore-binding binding snapshot)
(unless (tp-binding-disposed binding)
(tp--register-binding binding)
(dolist (dependency (tp-binding-dependencies binding))
(tp--subscription-add dependency binding)))))))
(defun tp--transaction-noop-final-accept ()
"Accept a pure reactive transaction with no external publication work."
nil)
(defun tp--transaction-precommit-function-p (function)
"Return non-nil when FUNCTION is a declared TP-internal hook symbol."
(and (symbolp function)
(memq function tp--transaction-precommit-allowed-functions)
(fboundp function)))
(defun tp--transaction-register-precommit-function (function)
"Register declared TP-internal precommit FUNCTION once."
(unless (tp--transaction-precommit-function-p function)
(signal 'tp-reactive-error
(list :invalid-precommit-function function)))
(unless (memq function tp--transaction-precommit-functions)
(setq tp--transaction-precommit-functions
(append tp--transaction-precommit-functions (list function))))
function)
(defun tp--transaction-install-final-accept (function)
"Install the active transaction's single final-accept FUNCTION."
(unless tp--transaction-active
(signal 'tp-reactive-error (list :final-accept-outside-transaction)))
(unless (functionp function)
(signal 'wrong-type-argument (list 'functionp function)))
(unless (eq tp--transaction-final-accept-function
#'tp--transaction-noop-final-accept)
(signal 'tp-reactive-error
(list :duplicate-final-accept-function
tp--transaction-final-accept-function function)))
(setq tp--transaction-final-accept-function function))
(defun tp--transaction-current-outcome-cell ()
"Return the active transaction's caller-retainable one-slot outcome cell."
(unless tp--transaction-active
(signal 'tp-reactive-error (list :outcome-cell-outside-transaction)))
tp--transaction-outcome-cell)
(defun tp--transaction-enter-phase (phase)
"Record the completed phase duration and enter PHASE."
(let ((now (float-time)))
(when (and tp--transaction-phase tp--transaction-phase-start)
(push (cons tp--transaction-phase
(- now tp--transaction-phase-start))
tp--transaction-phase-timings))
(setq tp--transaction-phase phase
tp--transaction-phase-start now)))
(defun tp--transaction-publish-outcome (outcome)
"Publish internal OUTCOME without changing the public transaction return."
(setq tp--transaction-outcome outcome
tp--last-transaction-outcome outcome)
(when (vectorp tp--transaction-outcome-cell)
(aset tp--transaction-outcome-cell 0 outcome))
(when tp--transaction-publication-batch
(setf (tp-publication-batch-candidate-outcome
tp--transaction-publication-batch)
outcome))
outcome)
(defun tp--transaction-batch-journal-view (surface-journals)
"Return a fixed view vector over current journals and SURFACE-JOURNALS."
(vector tp--transaction-extensions
tp--transaction-signal-values
tp--transaction-binding-snapshots
tp--transaction-counter-start
tp--transaction-signal-commit-journal
surface-journals))
(defun tp--transaction-begin-publication-batch
(batch-id entries surface-journals stage-entries)
"Install BATCH-ID for ENTRIES, SURFACE-JOURNALS, and STAGE-ENTRIES."
(unless tp--transaction-active
(signal 'tp-reactive-error (list :batch-outside-transaction batch-id)))
(when tp--transaction-publication-batch
(signal 'tp-publication-state-error
(list :duplicate-transaction-batch batch-id)))
(setq tp--transaction-publication-batch
(tp--publication-batch-prepare
:transaction-id tp--transaction-id
:batch-id batch-id
:entries entries
:participants
tp--transaction-structured-participants
:journals (tp--transaction-batch-journal-view surface-journals)
:stage-entries stage-entries
:final-accept tp--transaction-final-accept-function
:diagnostics nil))
tp--transaction-publication-batch)
(defun tp--transaction-batch-transition (next)
"Move the active publication candidate to NEXT when one exists."
(when tp--transaction-publication-batch
(tp--publication-batch-transition
tp--transaction-publication-batch next)))
(cl-defun tp--transaction-register-final-marker
(&key owner-key expected-token expected-version next-values inverse-values
slot-write-count operation-key)
"Register a bounded opaque marker for OWNER-KEY before precommit.
EXPECTED-TOKEN and EXPECTED-VERSION bind owner state. NEXT-VALUES and
INVERSE-VALUES are prebuilt opaque payloads. SLOT-WRITE-COUNT is checked
against the trusted OPERATION-KEY descriptor and the transaction bound."
(unless (and tp--transaction-active
(memq tp--transaction-phase
'(body recompute publication participants))
(not tp--transaction-final-markers-frozen-p))
(signal 'tp-final-marker-error
(list :registration-phase tp--transaction-phase)))
(when (>= tp--transaction-final-marker-count tp--final-marker-max-count)
(signal 'tp-final-marker-error
(list :marker-count tp--transaction-final-marker-count)))
(let ((duplicate nil))
(dotimes (index tp--transaction-final-marker-count)
(when (equal owner-key
(aref tp--transaction-final-marker-owner-keys index))
(setq duplicate t)))
(when duplicate
(signal 'tp-final-marker-error (list :duplicate-owner-key owner-key))))
(let ((marker
(tp--final-accept-marker-create
:owner-key owner-key
:expected-token expected-token
:expected-version expected-version
:next-values next-values
:inverse-values inverse-values
:slot-write-count slot-write-count
:operation-key operation-key)))
(when (> (+ tp--transaction-final-marker-slot-writes slot-write-count)
tp--final-marker-max-slot-writes)
(signal 'tp-final-marker-error
(list :slot-write-bound
tp--transaction-final-marker-slot-writes
slot-write-count)))
(aset tp--transaction-final-marker-registry
tp--transaction-final-marker-count marker)
(aset tp--transaction-final-marker-owner-keys
tp--transaction-final-marker-count
(tp--copy-property-value owner-key))
(cl-incf tp--transaction-final-marker-count)
(cl-incf tp--transaction-final-marker-slot-writes slot-write-count)
marker))
(cl-defun tp-transaction-register-final-marker
(&key owner-key expected-token expected-version next-values inverse-values
slot-write-count operation-key)
"Register one bounded final-accept authority marker.
OWNER-KEY must be unique in the active transaction. EXPECTED-TOKEN and
EXPECTED-VERSION bind owner state; NEXT-VALUES and INVERSE-VALUES are prebuilt
slot-write vectors with fixed SLOT-WRITE-COUNT. OPERATION-KEY must resolve
through TP's closed marker-operation whitelist."
(tp--transaction-register-final-marker
:owner-key owner-key
:expected-token expected-token
:expected-version expected-version
:next-values next-values
:inverse-values inverse-values
:slot-write-count slot-write-count
:operation-key operation-key))
(defun tp--transaction-freeze-final-markers ()
"Validate and seal every marker before signal commit and final accept."
(when (and (> tp--transaction-final-marker-count 0)
(null tp--transaction-publication-batch))
(signal 'tp-final-marker-error (list :marker-without-publication-batch)))
(dotimes (index tp--transaction-final-marker-count)
(tp--final-accept-marker-validate
(aref tp--transaction-final-marker-registry index)))
(setq tp--transaction-final-markers-frozen-p t)
(when tp--transaction-publication-batch
(setf (tp-publication-batch-candidate-markers
tp--transaction-publication-batch)
(cons tp--transaction-final-marker-registry
tp--transaction-final-marker-count)))
tp--transaction-final-marker-count)
(defun tp--transaction-sync-publication-batch ()
"Refresh fixed batch view slots after precommit and signal journaling."
(when tp--transaction-publication-batch
(setf (tp-publication-batch-candidate-final-accept
tp--transaction-publication-batch)
tp--transaction-final-accept-function)
(let ((journals
(tp-publication-batch-candidate-journals
tp--transaction-publication-batch)))
(when (vectorp journals)
(aset journals 4 tp--transaction-signal-commit-journal)))))
(defun tp--transaction-prepare-success-outcome ()
"Preallocate the active batch's success evidence before final accept."
(when tp--transaction-publication-batch
(let ((candidate tp--transaction-publication-batch)
(text-operations 0)
(property-operations 0)
(touched-characters 0)
target-counts)
(dolist (entry (tp-publication-batch-candidate-entries candidate))
(let ((counts (tp-publication-target-entry-operation-counts entry)))
(unless counts
(signal 'tp-publication-binding-error
(list :missing-operation-counts
(tp-publication-target-entry-surface-id entry))))
(cl-incf text-operations (or (plist-get counts :text-operations) 0))
(cl-incf property-operations
(or (plist-get counts :property-operations) 0))
(cl-incf touched-characters
(or (plist-get counts :touched-characters) 0))
(push (tp--copy-property-value counts) target-counts)))
(setf
(tp-publication-batch-candidate-operation-counts candidate)
(list :targets
(length (tp-publication-batch-candidate-entries candidate))
:participants (length tp--transaction-participants)
:signals (length tp--transaction-signals)
:markers tp--transaction-final-marker-count
:text-operations text-operations
:property-operations property-operations
:touched-characters touched-characters
:target-counts (nreverse target-counts))
(tp-publication-batch-candidate-phase-timings candidate)
(nreverse (copy-sequence tp--transaction-phase-timings))
(tp-publication-batch-candidate-diagnostics candidate)
(tp--copy-property-value tp--transaction-contained-failures))
(setf (tp-publication-batch-candidate-success-outcome-draft candidate)
(tp--committed-success-outcome-draft
candidate
(tp-publication-batch-candidate-operation-counts candidate)
(tp-publication-batch-candidate-phase-timings candidate)
(tp-publication-batch-candidate-diagnostics candidate)
tp--transaction-final-marker-count)))))
(defun tp--transaction-apply-one-final-marker (marker)
"Apply one prevalidated MARKER through its closed package primitive."
(funcall
(tp--final-marker-operation-apply
(tp-final-accept-marker-operation marker))
marker))
(defun tp--transaction-restore-one-final-marker (marker)
"Restore one prevalidated MARKER through its closed package primitive."
(funcall
(tp--final-marker-operation-restore
(tp-final-accept-marker-operation marker))
marker))
(defun tp--transaction-restore-final-marker-at (index failures-cell)
"Restore marker INDEX, then exhaustively continue using FAILURES-CELL."
(when (>= index 0)
(let ((marker (aref tp--transaction-final-marker-registry index)))
(unwind-protect
(when (memq (tp-final-accept-marker-state marker)
'(applying applied))
(setf (tp-final-accept-marker-state marker) 'restoring)
(condition-case failure
(progn
(tp--transaction-restore-one-final-marker marker)
(setf (tp-final-accept-marker-state marker) 'restored))
((error quit)
(setf (tp-final-accept-marker-state marker) 'restore-failed)
(aset
failures-cell 0
(cons (list 'final-markers
(tp-final-accept-marker-owner-key marker)
failure)
(aref failures-cell 0))))))
;; This cleanup runs even when a test or corrupted primitive exits by
;; an arbitrary nonlocal throw, so no earlier applied marker is skipped.
(tp--transaction-restore-final-marker-at
(1- index) failures-cell)))))
(defun tp--transaction-restore-applied-final-markers ()
"Reverse every applied marker and return contained restore failures."
(let ((index (1- tp--transaction-applied-final-marker-count))
(failures-cell (vector nil)))
(setq tp--transaction-applied-final-marker-count 0)
(tp--transaction-restore-final-marker-at index failures-cell)
(nreverse (aref failures-cell 0))))
(defun tp--transaction-apply-final-markers ()
"Apply every sealed marker in registration order."
(dotimes (index tp--transaction-final-marker-count)
(let ((marker (aref tp--transaction-final-marker-registry index)))
;; Count and mark first so mutate-then-signal is still reverse-restored.
(setq tp--transaction-applied-final-marker-count (1+ index))
(setf (tp-final-accept-marker-state marker) 'applying)
(tp--transaction-apply-one-final-marker marker)
(setf (tp-final-accept-marker-state marker) 'applied))))
(defun tp--transaction-commit-final-markers ()
"Finalize marker state through fixed writes after successful accept."
(dotimes (index tp--transaction-final-marker-count)
(setf (tp-final-accept-marker-state
(aref tp--transaction-final-marker-registry index))
'committed))
(setq tp--transaction-applied-final-marker-count 0))
(defun tp--transaction-run-final-accept ()
"Apply markers, invoke the existing single final accept, and finalize tags."
(tp--transaction-batch-transition 'final-accepting)
(let (accepted)
(unwind-protect
(progn
(tp--transaction-apply-final-markers)
(if tp--transaction-publication-batch
(let ((candidate-function
(tp-publication-batch-candidate-final-accept
tp--transaction-publication-batch)))
(unless (eq candidate-function
tp--transaction-final-accept-function)
(signal 'tp-publication-binding-error
(list :final-accept candidate-function
tp--transaction-final-accept-function)))
(funcall candidate-function))
(funcall tp--transaction-final-accept-function))
(setq accepted t))
(unless accepted
(setq tp--transaction-marker-restore-failures
(tp--transaction-restore-applied-final-markers))))
(when accepted
(tp--transaction-commit-final-markers)
(when tp--transaction-publication-batch
(let* ((candidate tp--transaction-publication-batch)
(outcome
(tp-publication-batch-candidate-success-outcome-draft
candidate)))
;; Every fallible validation and allocation happened before accept.
(setf (tp-publication-batch-candidate-state candidate) 'committed
(tp-publication-batch-candidate-resolution candidate) 'committed)
;; The success tag is a read-only slot with one coordinator-owned
;; fixed write after the existing final accept has returned.
(aset outcome tp--committed-success-outcome-tag-slot
'committed-success)
(tp--transaction-publish-outcome outcome))))))
(defun tp--transaction-run-shadow-proof (phase)
"Compare structured artifacts with the selected single live result for PHASE."
(when tp--transaction-publication-batch
(let ((ok t) results outcome-equivalent)
(dolist (entry
(tp-publication-batch-candidate-entries
tp--transaction-publication-batch))
(let ((validator (tp-publication-target-entry-shadow-validator entry)))
(condition-case failure
(let ((result (and validator (funcall validator entry phase))))
(setf (tp-publication-target-entry-shadow-actual entry) result
(tp-publication-target-entry-shadow-proven-p entry)
(and result (plist-get result :equivalent)))
(unless (tp-publication-target-entry-shadow-proven-p entry)
(setq ok nil))
(when (eq phase 'rollback)
(setf (tp-publication-target-entry-rollback-result entry)
(if (plist-get result :equivalent) 'restored 'mismatch)
(tp-publication-target-entry-post-rollback-state entry)
(plist-get result :actual)))
(push result results))
((error quit)
(setq ok nil)
(push (list :equivalent nil :failure failure) results)))))
(setq outcome-equivalent
(pcase phase
('commit
(tp--committed-success-outcome-valid-for-p
tp--transaction-outcome tp--transaction-publication-batch))
('rollback
(and tp--transaction-outcome
(tp--publication-failure-outcome-valid-for-p
tp--transaction-outcome
tp--transaction-publication-batch)))))
(when (and (eq phase 'commit) (not outcome-equivalent))
(setq ok nil))
(let ((proof (list :phase phase :equivalent ok
:outcome-equivalent outcome-equivalent
:entries (nreverse results))))
(setf (tp-publication-batch-candidate-shadow-proof
tp--transaction-publication-batch)
proof)
(setq tp--last-shadow-proof
(list :phase phase :equivalent ok
:outcome-equivalent outcome-equivalent
:entry-count
(length
(tp-publication-batch-candidate-entries
tp--transaction-publication-batch))))
(unless ok
(push (list 'shadow-proof phase proof)
tp--transaction-contained-failures))
proof))))
(defun tp--transaction-finalize-rollback-shadow-outcome ()
"Correlate the failure outcome with the already compared rollback artifacts."
(when-let* ((candidate tp--transaction-publication-batch)
(proof (tp-publication-batch-candidate-shadow-proof candidate)))
(let* ((outcome-equivalent
(tp--publication-failure-outcome-valid-for-p
tp--transaction-outcome candidate))
(equivalent
(and (plist-get proof :equivalent) outcome-equivalent)))
(setq proof (plist-put proof :outcome-equivalent outcome-equivalent)
proof (plist-put proof :equivalent equivalent))
(setf (tp-publication-batch-candidate-shadow-proof candidate) proof)
(setq tp--last-shadow-proof
(list :phase 'rollback :equivalent equivalent
:outcome-equivalent outcome-equivalent
:entry-count
(length (tp-publication-batch-candidate-entries candidate))))
(unless equivalent
(push (list 'shadow-proof 'rollback-outcome proof)
tp--transaction-contained-failures))
proof)))
(defun tp--transaction-finish-rollback (primary-condition rollback-failures)
"Finalize failure evidence for PRIMARY-CONDITION and ROLLBACK-FAILURES."
(dotimes (index tp--transaction-final-marker-count)
(let ((marker (aref tp--transaction-final-marker-registry index)))
(when (eq (tp-final-accept-marker-state marker) 'prepared)
(setf (tp-final-accept-marker-state marker) 'rolled-back))))
(when tp--transaction-publication-batch
(let ((candidate tp--transaction-publication-batch))
(unless (tp--publication-batch-terminal-p candidate)
(if (eq (tp-publication-batch-candidate-state candidate) 'prepared)
(progn
(setf (tp-publication-batch-candidate-state candidate) 'discarded
(tp-publication-batch-candidate-resolution candidate)
'discarded))
(tp--publication-batch-transition candidate 'rolled-back)))
(tp--transaction-run-shadow-proof 'rollback)
(when (and primary-condition
(eq (tp-publication-batch-candidate-state candidate)
'rolled-back))
(tp--transaction-publish-outcome
(tp--publication-failure-outcome-create
candidate tp--transaction-phase primary-condition rollback-failures
tp--transaction-contained-failures))
(tp--transaction-finalize-rollback-shadow-outcome)))))
(defun tp--run-contained-transaction-functions (phase functions)
"Run postaccept PHASE FUNCTIONS and record contained failures."
(dolist (function (tp--transaction-hook-functions functions))
(let ((inhibit-quit t)
(quit-flag nil))
(condition-case failure
(funcall function)
((error quit)
(push (list phase function failure)
tp--transaction-contained-failures))))))
(defun tp--run-transaction-precommit-functions ()
"Run the internal declared precommit registry in deterministic order."
(dolist (function tp--transaction-precommit-functions)
(unless (tp--transaction-precommit-function-p function)
(signal 'tp-reactive-error
(list :invalid-precommit-function function)))
(funcall function)))
(defun tp--prepare-signal-commit-journal ()
"Capture every touched signal before any committed field is mutated."
(setq tp--transaction-signal-commit-journal
(mapcar
(lambda (signal)
(tp--make-signal-commit-entry
:signal signal
:old-committed-value (tp-signal-committed-value signal)
:old-revision (tp-signal-revision signal)
:candidate-value
(gethash signal tp--transaction-signal-values)))
(nreverse (copy-sequence tp--transaction-signals)))))
(defun tp--commit-signal-entry (entry)
"Commit signal journal ENTRY exactly once."
(let ((signal (tp--signal-commit-entry-signal entry)))
(setf (tp-signal-committed-value signal)
(tp--signal-commit-entry-candidate-value entry)
(tp-signal-revision signal)
(1+ (tp--signal-commit-entry-old-revision entry)))))
(defun tp--restore-signal-entry (entry)
"Restore signal journal ENTRY's exact committed state."
(let ((signal (tp--signal-commit-entry-signal entry)))
(setf (tp-signal-committed-value signal)
(tp--signal-commit-entry-old-committed-value entry)
(tp-signal-revision signal)
(tp--signal-commit-entry-old-revision entry))))
(defun tp--commit-signal-values ()
"Journal and commit candidate signals in stable first-touch order."
(tp--prepare-signal-commit-journal)
(dolist (entry tp--transaction-signal-commit-journal)
(tp--commit-signal-entry entry)))
(defun tp--restore-transaction-counters (snapshot)
"Restore reactive counters from SNAPSHOT."
(setq tp--reactive-counters snapshot))
(defun tp--transaction-hook-functions (value)
"Return hook VALUE as one ordered function list."
(cond
((null value) nil)
((functionp value) (list value))
(t value)))
(defun tp--rollback-hook-phase (phase functions)
"Run rollback PHASE FUNCTIONS and return ordered failures."
(let (failures)
(dolist (function (tp--transaction-hook-functions functions))
(condition-case failure
(funcall function)
((error quit)
(push (list phase function failure) failures))))
(nreverse failures)))
(defun tp--rollback-signal-journal ()
"Restore every signal journal entry and return ordered failures."
(let (failures)
(dolist (entry tp--transaction-signal-commit-journal)
(condition-case failure
(tp--restore-signal-entry entry)
((error quit)
(push (list 'signal-journal
(tp-signal-id
(tp--signal-commit-entry-signal entry))
failure)
failures))))
(nreverse failures)))
(defun tp--rollback-transaction-state (counter-snapshot)
"Restore COUNTER-SNAPSHOT through every rollback phase.
Return contained failures in phase order."
(let ((failures (copy-sequence tp--transaction-marker-restore-failures)))
(setq failures
(append
failures
(tp--rollback-transaction-participants)
(tp--rollback-hook-phase
'rollback-hooks tp--transaction-rollback-functions)
(tp--rollback-signal-journal)))
(condition-case failure
(tp--rollback-bindings)
((error quit)
(setq failures
(append failures
(list (list 'bindings 'binding-graph failure))))))
(condition-case failure
(tp--restore-transaction-counters counter-snapshot)
((error quit)
(setq failures
(append failures
(list (list 'counters 'reactive-counters failure))))))
(append
failures
(tp--rollback-hook-phase
'rollback-final tp--transaction-rollback-final-functions))))
(defun tp--resignal-transaction-primary (primary-condition failures)
"Re-signal PRIMARY-CONDITION with ordered rollback FAILURES attached."
(signal (car primary-condition)
(if failures
(append (cdr primary-condition)
(list tp--transaction-condition-trailer-tag
(list :rollback-failures failures)))
(cdr primary-condition))))
(defun tp--transaction-condition-trailer (condition property)
"Return PROPERTY from the exact transaction trailer of CONDITION.
Transaction rollback metadata is appended after, and never merged into,
the primary condition data."
(let ((trailer (last condition 2)))
(and (eq (car trailer) tp--transaction-condition-trailer-tag)
(plist-get (cadr trailer) property))))
(defun tp--call-with-transaction (function)
"Call FUNCTION in one atomic signal and binding transaction."
(if tp--transaction-active
(funcall function)
(let (after-commit result
(tp--transaction-contained-failures nil))
(setq tp--last-transaction-outcome nil
tp--last-shadow-proof nil)
(setq result
(let ((tp--transaction-active t)
(tp--transaction-id (tp--next-transaction-id))
(tp--transaction-phase 'body)
(tp--transaction-phase-start (float-time))
(tp--transaction-phase-timings nil)
(tp--transaction-signal-values
(make-hash-table :test #'eq))
(tp--transaction-signals nil)
(tp--transaction-dirty-set (make-hash-table :test #'eq))
(tp--transaction-dirty-queue nil)
(tp--transaction-binding-snapshots
(make-hash-table :test #'eq))
(tp--transaction-created-bindings nil)
(tp--transaction-recompute-counts
(make-hash-table :test #'eq))
(tp--transaction-extensions (make-hash-table :test #'eq))
(tp--transaction-after-commit-callbacks nil)
(tp--transaction-participants nil)
(tp--transaction-participant-keys nil)
(tp--transaction-participant-order 0)
(tp--transaction-published-participants nil)
(tp--transaction-signal-commit-journal nil)
(tp--transaction-publication-batch nil)
(tp--transaction-structured-participants nil)
(tp--transaction-final-marker-registry
(make-vector tp--final-marker-max-count nil))
(tp--transaction-final-marker-owner-keys
(make-vector tp--final-marker-max-count nil))
(tp--transaction-final-marker-count 0)
(tp--transaction-final-marker-slot-writes 0)
(tp--transaction-final-markers-frozen-p nil)
(tp--transaction-applied-final-marker-count 0)
(tp--transaction-marker-restore-failures nil)
(tp--transaction-outcome nil)
(tp--transaction-outcome-cell (vector nil))
(tp--transaction-final-accept-function
#'tp--transaction-noop-final-accept)
(counter-snapshot (copy-sequence tp--reactive-counters))
(tp--transaction-counter-start nil)
success transaction-result primary-condition
rollback-failures pending-quit)
(setq tp--transaction-counter-start counter-snapshot)
(unwind-protect
(condition-case condition
(progn
(setq transaction-result (funcall function))
(tp--transaction-enter-phase 'recompute)
(tp--flush-dirty-bindings)
(tp--transaction-enter-phase 'publication)
(setq tp--transaction-structured-participants
(vconcat
(tp--transaction-participants-in-registration-order)))
(run-hooks 'tp--transaction-publish-functions)
(tp--transaction-batch-transition 'participants)
(tp--transaction-enter-phase 'participants)
(tp--stage-structured-transaction-participants)
(tp--transaction-batch-transition 'precommit)
(tp--transaction-enter-phase 'precommit)
(tp--run-structured-transaction-participant-precommits)
(tp--run-transaction-precommit-functions)
(tp--transaction-freeze-final-markers)
(tp--transaction-sync-publication-batch)
(tp--transaction-enter-phase 'signal-commit)
(tp--commit-signal-values)
(tp--transaction-sync-publication-batch)
(tp--transaction-enter-phase 'final-accept)
(tp--transaction-prepare-success-outcome)
(condition-case deferred-quit
(progn
(let ((inhibit-quit t)
(quit-flag nil))
(unwind-protect
(progn
(unless
(functionp
tp--transaction-final-accept-function)
(signal
'tp-reactive-error
(list
:invalid-final-accept-function
tp--transaction-final-accept-function)))
(tp--transaction-run-final-accept)
(setq success t)
(tp--commit-structured-transaction-participants)
(tp--transaction-run-shadow-proof 'commit)
(when quit-flag
(setq pending-quit t
quit-flag nil))
(tp--run-contained-transaction-functions
'committed
tp--transaction-committed-functions)
(setq after-commit
(nreverse
tp--transaction-after-commit-callbacks))
(when pending-quit
(push
(list 'final-accept
tp--transaction-final-accept-function
'(quit))
tp--transaction-contained-failures)))
(setq quit-flag nil)))
nil)
(quit
(if success
(push
(list 'final-accept
tp--transaction-final-accept-function
deferred-quit)
tp--transaction-contained-failures)
(signal (car deferred-quit)
(cdr deferred-quit))))))
((error quit)
(setq primary-condition condition)))
(unless success
(let ((inhibit-quit t)
(quit-flag nil))
(unwind-protect
(progn
(setq tp--transaction-phase
(or tp--transaction-phase 'rollback)
rollback-failures
(tp--rollback-transaction-state
counter-snapshot))
(tp--transaction-finish-rollback
primary-condition rollback-failures))
(setq quit-flag nil)))))
(when primary-condition
(tp--resignal-transaction-primary
primary-condition rollback-failures))
transaction-result))
(tp--run-contained-transaction-functions 'after-commit after-commit)
(setq tp--last-transaction-diagnostics
(nreverse tp--transaction-contained-failures))
result)))
;;;###autoload
(defmacro tp-with-transaction (&rest body)
"Evaluate BODY in one atomic signal and binding transaction."
(declare (indent 0) (debug t))
`(tp--call-with-transaction (lambda () ,@body)))
(defun tp--variable-signal-key (symbol scope)
"Return the adapter key for SYMBOL and SCOPE."
(cons symbol scope))
(defun tp--variable-signal-watcher (symbol new-value operation where)
"Forward SYMBOL's NEW-VALUE write into its exact signal adapter.
OPERATION and WHERE follow the standard variable watcher protocol."
(when (eq operation 'set)
(when-let* ((signal (gethash (tp--variable-signal-key
symbol (or where 'global))
tp--variable-signals)))
(when (tp-signal-live-p signal)
(tp-signal-set signal new-value)))))
(defun tp--variable-signal-initial-value (symbol scope)
"Return SYMBOL's initial adapter value in SCOPE."
(if (eq scope 'global)
(if (default-boundp symbol) (default-value symbol) nil)
(with-current-buffer scope
(if (boundp symbol) (symbol-value symbol) nil))))
(defun tp-variable-signal (symbol &optional buffer)
"Return the signal adapting SYMBOL globally or in BUFFER."
(unless (symbolp symbol)
(signal 'wrong-type-argument (list 'symbolp symbol)))
(when (and buffer (not (buffer-live-p buffer)))
(signal 'tp-invalid-signal-scope (list buffer)))
(let* ((scope (or buffer 'global))
(key (tp--variable-signal-key symbol scope)))
(or (gethash key tp--variable-signals)
(let ((signal (tp-signal-create
(tp--variable-signal-initial-value symbol scope)
:scope scope)))
(setf (tp-signal-adapter-key signal) key)
(puthash key signal tp--variable-signals)
(unless (memq symbol tp--variable-signal-watched)
(add-variable-watcher symbol #'tp--variable-signal-watcher)
(push symbol tp--variable-signal-watched))
signal))))
(defun tp--symbol-has-variable-signal-p (symbol)
"Return non-nil when SYMBOL still owns an adapter signal."
(let ((found nil))
(maphash (lambda (key _signal)
(when (eq (car key) symbol) (setq found t)))
tp--variable-signals)
found))
(defun tp--dispose-signal (signal)
"Dispose SIGNAL and detach its graph and adapter state."
(unless (tp-signal-disposed signal)
(dolist (binding (tp--sorted-subscribers signal))
(setf (tp-binding-dependencies binding)
(delq signal (tp-binding-dependencies binding))
(tp-binding-dirty binding) t))
(clrhash (tp-signal-subscribers signal))
(when-let* ((key (tp-signal-adapter-key signal)))
(remhash key tp--variable-signals)
(let ((symbol (car key)))
(unless (tp--symbol-has-variable-signal-p symbol)
(remove-variable-watcher symbol #'tp--variable-signal-watcher)
(setq tp--variable-signal-watched
(delq symbol tp--variable-signal-watched)))))
(when-let* ((scope (and (bufferp (tp-signal-scope signal))
(tp-signal-scope signal))))
(when (buffer-live-p scope)
(with-current-buffer scope
(setq tp--buffer-signals (delq signal tp--buffer-signals)))))
(setf (tp-signal-disposed signal) t)
(setq tp--signals (delq signal tp--signals))))
(defun tp-signal-dispose (signal)
"Dispose SIGNAL, detach its subscriptions, and return nil."
(unless (tp-signal-p signal)
(signal 'wrong-type-argument (list 'tp-signal-p signal)))
(when tp--transaction-active
(signal 'tp-reactive-error (list :dispose-during-transaction)))
(tp--dispose-signal signal)
nil)
(defun tp--reactive-graph-reset ()
"Clear all signal, binding, adapter, and scheduler state."
(when tp--transaction-active
(signal 'tp-reactive-error (list :reset-during-transaction)))
(dolist (binding tp--bindings)
(tp--detach-binding-dependencies binding)
(clrhash (tp-binding-subscribers binding))
(setf (tp-binding-disposed binding) t
(tp-binding-state binding) 'disposed))
(dolist (signal (copy-sequence tp--signals))
(tp--dispose-signal signal))
(clrhash tp--owner-bindings)
(clrhash tp--variable-signals)
(setq tp--bindings nil
tp--signals nil
tp--variable-signal-watched nil)
(tp-reactive-reset-counters))
;;;###autoload
(defun tp-reactive-reset ()
"Reset TP's signal, binding, adapter, and scheduler graph."
(interactive)
(tp--reactive-graph-reset))
(provide 'tp-reactive)
;;; tp-reactive.el ends here