From 0cde865d3a430535a7dc1c110ade43eca6d3fae6 Mon Sep 17 00:00:00 2001 From: Kinneyzhang Date: Tue, 1 Sep 2026 00:33:54 +0800 Subject: [PATCH] refactor: make semantic generation authoritative --- Makefile | 4 +- docs/architecture.en.md | 8 +++ docs/architecture.zh.md | 7 ++ etaf-generation.el | 89 +++++++++++++++++++++++ etaf-runtime.el | 121 ++++++++++++++++++++++++++++--- etaf.el | 1 + tests/etaf-generation-tests.el | 127 +++++++++++++++++++++++++++++++++ 7 files changed, 344 insertions(+), 13 deletions(-) create mode 100644 etaf-generation.el create mode 100644 tests/etaf-generation-tests.el diff --git a/Makefile b/Makefile index 26b4a89..6d57bbd 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ EMACS ?= emacs LOAD_PATH = -L . -L examples -L scripts -L ../ebox -L ../tp -L ../ecss -SOURCES = etaf-view.el etaf-compiler.el etaf-component.el etaf-reactive.el etaf-observer.el etaf-context.el etaf-theme-tp.el etaf-resource.el etaf-data.el etaf-render-port.el etaf-renderer.el etaf-runtime.el etaf-behavior.el etaf-actions.el etaf-events.el etaf-performance.el etaf.el scripts/emacs-gui-verifier.el +SOURCES = etaf-view.el etaf-compiler.el etaf-component.el etaf-reactive.el etaf-observer.el etaf-context.el etaf-theme-tp.el etaf-resource.el etaf-data.el etaf-generation.el etaf-render-port.el etaf-renderer.el etaf-runtime.el etaf-behavior.el etaf-actions.el etaf-events.el etaf-performance.el etaf.el scripts/emacs-gui-verifier.el EXAMPLES = examples/etaf-counter-example.el examples/etaf-data-example.el examples/etaf-resource-example.el -TESTS = tests/etaf-tests.el tests/etaf-compiler-tests.el tests/etaf-component-frontends-tests.el tests/etaf-resource-tests.el tests/etaf-data-tests.el tests/etaf-theme-tp-tests.el tests/etaf-examples-tests.el tests/etaf-observer-tests.el tests/etaf-performance-tests.el tests/etaf-gui-verifier-tests.el tests/etaf-m0a-current-characterization-tests.el tests/etaf-interaction-contract-tests.el tests/etaf-m0b-component-manifest-tests.el tests/etaf-render-port-tests.el +TESTS = tests/etaf-tests.el tests/etaf-compiler-tests.el tests/etaf-component-frontends-tests.el tests/etaf-resource-tests.el tests/etaf-data-tests.el tests/etaf-theme-tp-tests.el tests/etaf-examples-tests.el tests/etaf-observer-tests.el tests/etaf-performance-tests.el tests/etaf-gui-verifier-tests.el tests/etaf-m0a-current-characterization-tests.el tests/etaf-interaction-contract-tests.el tests/etaf-m0b-component-manifest-tests.el tests/etaf-render-port-tests.el tests/etaf-generation-tests.el .PHONY: test compile load checkdoc docs-check check clean diff --git a/docs/architecture.en.md b/docs/architecture.en.md index fad4088..2ced315 100644 --- a/docs/architecture.en.md +++ b/docs/architecture.en.md @@ -450,6 +450,14 @@ through one outer reactive dispatch. A local owner evaluates into a candidate generation and Ebox logical replacement; disjoint owners are coalesced into one TP/Ebox publication. The Root owner is the only complete-root adapter. +The sole mutable pointer to the committed semantic generation belongs to +`etaf-generation-authority`. Public handler and Host-property queries read that +generation directly. Same-named Runtime hash tables are one-way compatibility +mirrors rebuilt from the generation; they neither authorize queries nor write +back into it. The migration-only `legacy`, `project`, and `shadow` routes prove +projection equivalence and rollback safety without introducing a second +committed truth. + Each Runtime flush records a candidate-aware effect tuple containing the generation id, effect-to-source edges and source versions, plus an immutable semantic-node stamp for candidate input/context/output facts. A repeated tuple diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index fff9c02..4226055 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -440,6 +440,13 @@ mounted Component、expr、slot、fragment、raw、inline 和 Root owner 都经 replacement;不相交 owner 会合并为一次 TP/Ebox publication。只有 Root owner 可以进入 complete-root adapter。 +已提交 semantic generation 的唯一 mutable pointer 由 +`etaf-generation-authority` 持有。handler 与 Host-prop 等公共查询直接读取该 +generation;Runtime 中同名 hash table 只是由 generation 单向重建的兼容 mirror, +既不能独立授权查询,也不能反向改写 generation。迁移期的 +`legacy`/`project`/`shadow` 路由只用于证明投影等价与安全回退,不增加第二份 +committed truth。 + 每次 Runtime flush 都记录 candidate-aware effect tuple:其中包含 generation id、 effect→source 边和 source version,以及 candidate input/context/output facts 的 immutable semantic-node stamp。重复 tuple 会报告有序的 effect/edge path;step diff --git a/etaf-generation.el b/etaf-generation.el new file mode 100644 index 0000000..c111e6d --- /dev/null +++ b/etaf-generation.el @@ -0,0 +1,89 @@ +;;; etaf-generation.el --- ETAF committed generation authority -*- lexical-binding: t; -*- + +;; SPDX-License-Identifier: GPL-3.0-or-later + +;;; Commentary: + +;; Owns the single mutable pointer to ETAF's committed semantic generation and +;; pure construction/comparison of compatibility mirrors. Runtime mirrors are +;; projections only; they never authorize semantic queries or publication. + +;;; Code: + +(require 'cl-lib) +(require 'subr-x) + +(define-error 'etaf-generation-error "Invalid ETAF generation authority") + +(cl-defstruct + (etaf-generation-authority + (:constructor etaf-generation-authority--create)) + "Mutable authority for one Runtime's committed generation." + generation + (token 0)) + +(defun etaf-generation-authority-create (&optional generation) + "Return fresh authority initially pointing at GENERATION." + (etaf-generation-authority--create :generation generation :token 0)) + +(defun etaf-generation-authority-current (authority) + "Return AUTHORITY's current committed generation." + (unless (etaf-generation-authority-p authority) + (signal 'wrong-type-argument + (list 'etaf-generation-authority-p authority))) + (etaf-generation-authority-generation authority)) + +(defun etaf-generation-authority-set-current (authority generation) + "Set AUTHORITY's current committed GENERATION and return GENERATION. +This compatibility mutation does not advance the semantic commit token; the +versioned compare-and-swap boundary owns token changes." + (unless (etaf-generation-authority-p authority) + (signal 'wrong-type-argument + (list 'etaf-generation-authority-p authority))) + (setf (etaf-generation-authority-generation authority) generation) + generation) + +(defun etaf-generation-project-mirror (entries &optional test) + "Project immutable contribution ENTRIES into a fresh hash table. +TEST defaults to `equal'. Duplicate keys are rejected so a compatibility +mirror cannot silently choose a second semantic truth." + (unless (proper-list-p entries) + (signal 'wrong-type-argument (list 'proper-list-p entries))) + (let ((table (make-hash-table :test (or test #'equal))) + (missing (make-symbol "etaf-generation-mirror-missing"))) + (dolist (entry entries) + (unless (consp entry) + (signal 'etaf-generation-error + (list :malformed-mirror-entry entry))) + (let ((key (car entry))) + (unless (eq (gethash key table missing) missing) + (signal 'etaf-generation-error + (list :duplicate-mirror-key key))) + (puthash (copy-tree key) (copy-tree (cdr entry)) table))) + table)) + +(defun etaf-generation-project-mirrors (handler-entries host-prop-entries) + "Return fresh mirrors from HANDLER-ENTRIES and HOST-PROP-ENTRIES." + (list :handlers + (etaf-generation-project-mirror handler-entries #'equal) + :host-props + (etaf-generation-project-mirror host-prop-entries #'equal))) + +(defun etaf-generation-mirror-equal-p (left right) + "Return non-nil when hash tables LEFT and RIGHT contain equal facts." + (and (hash-table-p left) + (hash-table-p right) + (= (hash-table-count left) (hash-table-count right)) + (let ((missing (make-symbol "etaf-generation-mirror-missing")) + equal-p) + (setq equal-p t) + (maphash + (lambda (key value) + (unless (equal value (gethash key right missing)) + (setq equal-p nil))) + left) + equal-p))) + +(provide 'etaf-generation) + +;;; etaf-generation.el ends here diff --git a/etaf-runtime.el b/etaf-runtime.el index 25cf5df..af5e0b8 100644 --- a/etaf-runtime.el +++ b/etaf-runtime.el @@ -15,6 +15,7 @@ (require 'etaf-view) (require 'etaf-component) (require 'etaf-reactive) +(require 'etaf-generation) (require 'etaf-render-port) (require 'etaf-renderer) (require 'etaf-context) @@ -284,7 +285,7 @@ the sequential `etaf--pvec-put' contract." next-resource-id next-effect-id next-semantic-id - current-generation + generation-authority artifact-registry candidate-artifacts candidate-effects @@ -334,6 +335,33 @@ the sequential `etaf--pvec-put' contract." (event-depth 0) dirty-effect-queue-tail) +(defcustom etaf-generation-mirror-route 'project + "Compatibility mirror route used after a committed generation changes. +`legacy' preserves incremental Runtime table updates, `project' rebuilds exact +mirrors from the committed generation, and `shadow' runs legacy updates before +checking them against a fresh generation projection." + :type '(choice (const legacy) (const project) (const shadow)) + :group 'etaf) + +(defun etaf--runtime-generation-authority (runtime) + "Return RUNTIME's generation authority, creating its empty owner if needed." + (or (etaf-runtime-generation-authority runtime) + (setf (etaf-runtime-generation-authority runtime) + (etaf-generation-authority-create)))) + +(defun etaf-runtime-current-generation (runtime) + "Return RUNTIME's uniquely authoritative committed generation." + (etaf-generation-authority-current + (etaf--runtime-generation-authority runtime))) + +(defun etaf-runtime--set-current-generation (runtime generation) + "Set RUNTIME's compatibility generation pointer to GENERATION." + (etaf-generation-authority-set-current + (etaf--runtime-generation-authority runtime) generation)) + +(gv-define-simple-setter etaf-runtime-current-generation + etaf-runtime--set-current-generation) + (defun etaf--theme-paint-face-spec (property value) "Return PROPERTY's TP face spec for resolved paint VALUE, or nil." (pcase property @@ -572,6 +600,81 @@ RESOLVED is the current projected paint value." (copy-tree (etaf--generation-index-entries (etaf-runtime-current-generation runtime) 'host-props))) +(defun etaf--runtime-generation-mirror-projection (generation) + "Return exact compatibility mirrors projected from GENERATION." + (etaf-generation-project-mirrors + (etaf--generation-index-entries generation 'handlers) + (etaf--generation-index-entries generation 'host-props))) + +(defun etaf--runtime-install-legacy-generation-mirrors (runtime full-p) + "Install RUNTIME candidate mirrors through the legacy FULL-P route." + (if full-p + (setf (etaf-runtime-handlers runtime) + (etaf-runtime-candidate-handlers runtime) + (etaf-runtime-host-props runtime) + (etaf-runtime-candidate-host-props runtime)) + (unless (hash-table-p (etaf-runtime-handlers runtime)) + (setf (etaf-runtime-handlers runtime) (make-hash-table :test #'equal))) + (unless (hash-table-p (etaf-runtime-host-props runtime)) + (setf (etaf-runtime-host-props runtime) + (make-hash-table :test #'equal))) + (maphash + (lambda (host-ref handlers) + (puthash host-ref handlers (etaf-runtime-handlers runtime))) + (etaf-runtime-candidate-handlers runtime)) + (maphash + (lambda (host-ref props) + (puthash host-ref props (etaf-runtime-host-props runtime))) + (etaf-runtime-candidate-host-props runtime)))) + +(defun etaf--runtime-install-projected-generation-mirrors + (runtime projection) + "Install detached generation mirror PROJECTION into RUNTIME." + (setf (etaf-runtime-handlers runtime) (plist-get projection :handlers) + (etaf-runtime-host-props runtime) (plist-get projection :host-props))) + +(defun etaf-runtime-generation-mirrors-consistent-p (runtime) + "Return non-nil when RUNTIME mirrors equal its committed generation." + (let ((projection + (etaf--runtime-generation-mirror-projection + (etaf-runtime-current-generation runtime)))) + (and + (etaf-generation-mirror-equal-p + (or (etaf-runtime-handlers runtime) (make-hash-table :test #'equal)) + (plist-get projection :handlers)) + (etaf-generation-mirror-equal-p + (or (etaf-runtime-host-props runtime) (make-hash-table :test #'equal)) + (plist-get projection :host-props))))) + +(defun etaf--runtime-install-generation-mirrors + (runtime generation full-p) + "Install RUNTIME compatibility mirrors for committed GENERATION. +FULL-P preserves the old route's full-root replacement distinction." + (let ((projection (etaf--runtime-generation-mirror-projection generation))) + (pcase etaf-generation-mirror-route + ('legacy + (etaf--runtime-install-legacy-generation-mirrors runtime full-p)) + ('project + (etaf--runtime-install-projected-generation-mirrors runtime projection)) + ('shadow + (etaf--runtime-install-legacy-generation-mirrors runtime full-p) + (unless (and + (etaf-generation-mirror-equal-p + (etaf-runtime-handlers runtime) + (plist-get projection :handlers)) + (etaf-generation-mirror-equal-p + (etaf-runtime-host-props runtime) + (plist-get projection :host-props))) + (signal 'etaf-generation-error + (list :compatibility-mirror-drift + :generation + (and generation + (etaf-generation-generation-id generation)))))) + (_ + (signal 'etaf-generation-error + (list :unknown-mirror-route etaf-generation-mirror-route)))) + runtime)) + (defun etaf--runtime-build-contribution-indexes (runtime base full-p) "Build RUNTIME immutable generation contributions over BASE. FULL-P means candidate tables describe the complete mounted tree." @@ -5592,12 +5695,8 @@ RENDERED-IDENTITIES names the Component render participants." (etaf--runtime-clear-candidate runtime) (signal (car err) (cdr err)))) (etaf--runtime-complete-generation runtime candidate-generation) - (maphash (lambda (host-ref handlers) - (puthash host-ref handlers (etaf-runtime-handlers runtime))) - (etaf-runtime-candidate-handlers runtime)) - (maphash (lambda (host-ref props) - (puthash host-ref props (etaf-runtime-host-props runtime))) - (etaf-runtime-candidate-host-props runtime)) + (etaf--runtime-install-generation-mirrors + runtime candidate-generation nil) (etaf--runtime-clear-dirty-effects runtime) (unwind-protect (progn @@ -5670,11 +5769,9 @@ RENDERED-IDENTITIES names the Component render participants." (list :observer #'etaf--runtime-forward-ebox-report))) (etaf--runtime-participant-publish participant)) (etaf--runtime-complete-generation runtime candidate-generation) + (etaf--runtime-install-generation-mirrors + runtime candidate-generation t) (setf (etaf-runtime-root-node runtime) root-node - (etaf-runtime-handlers runtime) - (etaf-runtime-candidate-handlers runtime) - (etaf-runtime-host-props runtime) - (etaf-runtime-candidate-host-props runtime) (etaf-runtime-root-dirty-p runtime) nil) (etaf--runtime-clear-dirty-effects runtime)) ((error quit) @@ -5770,6 +5867,8 @@ backend anchor proof failed; ordinary root turns keep their artifact reuse." :next-resource-id 0 :next-effect-id 0 :next-semantic-id 1 + :generation-authority + (etaf-generation-authority-create) :root-effect-id 0 :root-range-id 1 :artifact-registry (make-hash-table :test #'equal) diff --git a/etaf.el b/etaf.el index 713dddf..6202cfb 100644 --- a/etaf.el +++ b/etaf.el @@ -35,6 +35,7 @@ (require 'etaf-context) (require 'etaf-resource) (require 'etaf-data) +(require 'etaf-generation) (require 'etaf-render-port) (require 'etaf-renderer) (etaf--prefer-local-files) diff --git a/tests/etaf-generation-tests.el b/tests/etaf-generation-tests.el new file mode 100644 index 0000000..88e1d4f --- /dev/null +++ b/tests/etaf-generation-tests.el @@ -0,0 +1,127 @@ +;;; etaf-generation-tests.el --- M3a generation authority gates -*- lexical-binding: t; -*- + +;; SPDX-License-Identifier: GPL-3.0-or-later + +(require 'ert) +(require 'etaf) +(require 'etaf-generation) + +(defun etaf-generation-test--view (visible) + "Return a root View selected by reactive VISIBLE." + (if (etaf-value visible) + (etaf-view + (text :ref 'generation-target :role 'button + :on-press #'ignore "visible")) + (etaf-view (text :ref 'generation-other "other")))) + +(ert-deftest etaf-generation-authority-is-runtime-single-source () + "Runtime compatibility access reads and writes one authority object." + (let* ((runtime (etaf--runtime-create)) + (first (etaf--generation-create :generation-id 1)) + (second (etaf--generation-create :generation-id 2))) + (should-not (etaf-runtime-current-generation runtime)) + (setf (etaf-runtime-current-generation runtime) first) + (let ((authority (etaf-runtime-generation-authority runtime))) + (should (etaf-generation-authority-p authority)) + (should (eq first (etaf-generation-authority-current authority))) + (should (eq first (etaf-runtime-current-generation runtime))) + (setf (etaf-runtime-current-generation runtime) second) + (should (eq authority (etaf-runtime-generation-authority runtime))) + (should (eq second (etaf-runtime-current-generation runtime))) + (should (zerop (etaf-generation-authority-token authority)))))) + +(ert-deftest etaf-generation-projection-is-detached-and-rejects-duplicates () + "Compatibility projections copy facts and reject ambiguous keys." + (let* ((handlers '((host . ((press . callback))))) + (projection + (etaf-generation-project-mirrors handlers '((host :role button)))) + (handler-table (plist-get projection :handlers)) + (props-table (plist-get projection :host-props))) + (setcdr (car handlers) 'mutated) + (should (equal (gethash 'host handler-table) '((press . callback)))) + (should (equal (gethash 'host props-table) '(:role button))) + (should-error + (etaf-generation-project-mirror '((host . one) (host . two))) + :type 'etaf-generation-error))) + +(ert-deftest etaf-generation-mirrors-follow-committed-generation () + "Every commit projects exact mirrors and removes stale Host facts." + (let ((buffer-name " *etaf-generation-mirror-test*") + (visible (etaf-ref t))) + (unwind-protect + (progn + (etaf-mount buffer-name + (lambda () (etaf-generation-test--view visible))) + (let ((runtime (etaf-runtime-for-buffer buffer-name))) + (should (etaf-runtime-generation-mirrors-consistent-p runtime)) + (should (etaf-runtime-handler-for runtime 'generation-target)) + (should (gethash 'generation-target + (etaf-runtime-handlers runtime))) + (setf (etaf-value visible) nil) + (should (etaf-runtime-generation-mirrors-consistent-p runtime)) + (should-not (etaf-runtime-handler-for runtime 'generation-target)) + (should-not (gethash 'generation-target + (etaf-runtime-handlers runtime))) + (should (gethash 'generation-other + (etaf-runtime-host-props runtime))))) + (when-let* ((runtime (etaf-runtime-for-buffer buffer-name))) + (etaf-unmount runtime)) + (when-let* ((buffer (get-buffer buffer-name))) + (kill-buffer buffer))))) + +(ert-deftest etaf-generation-query-ignores-compatibility-mirror-drift () + "Corrupting a mirror never changes committed generation queries." + (let ((buffer-name " *etaf-generation-query-test*") + (visible (etaf-ref t))) + (unwind-protect + (progn + (etaf-mount buffer-name + (lambda () (etaf-generation-test--view visible))) + (let* ((runtime (etaf-runtime-for-buffer buffer-name)) + (committed + (copy-tree + (etaf-runtime-handler-for runtime 'generation-target)))) + (puthash 'generation-target 'corrupt + (etaf-runtime-handlers runtime)) + (should-not (etaf-runtime-generation-mirrors-consistent-p runtime)) + (should (equal committed + (etaf-runtime-handler-for + runtime 'generation-target))) + (setf (etaf-value visible) nil) + (setf (etaf-value visible) t) + (should (etaf-runtime-generation-mirrors-consistent-p runtime)))) + (when-let* ((runtime (etaf-runtime-for-buffer buffer-name))) + (etaf-unmount runtime)) + (when-let* ((buffer (get-buffer buffer-name))) + (kill-buffer buffer))))) + +(ert-deftest etaf-generation-shadow-route-proves-legacy-equivalence () + "The shadow route accepts a legacy projection only when facts are equal." + (let ((buffer-name " *etaf-generation-shadow-test*") + (visible (etaf-ref t)) + (etaf-generation-mirror-route 'shadow)) + (unwind-protect + (progn + (etaf-mount buffer-name + (lambda () (etaf-generation-test--view visible))) + (setf (etaf-value visible) nil) + (let ((runtime (etaf-runtime-for-buffer buffer-name))) + (should (etaf-runtime-generation-mirrors-consistent-p runtime)))) + (when-let* ((runtime (etaf-runtime-for-buffer buffer-name))) + (etaf-unmount runtime)) + (when-let* ((buffer (get-buffer buffer-name))) + (kill-buffer buffer))))) + +(ert-deftest etaf-generation-rejects-unknown-mirror-route () + "An unknown compatibility route cannot silently publish mirrors." + (let ((runtime (etaf--runtime-create + :generation-authority + (etaf-generation-authority-create))) + (etaf-generation-mirror-route 'unknown)) + (should-error + (etaf--runtime-install-generation-mirrors runtime nil t) + :type 'etaf-generation-error))) + +(provide 'etaf-generation-tests) + +;;; etaf-generation-tests.el ends here