No description
This repository has been archived on 2026-09-25. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
  • Emacs Lisp 95.6%
  • Python 3.9%
  • Makefile 0.5%
Find a file
Kinneyzhang 8158ef1f43
Some checks failed
Repository structure / structure (push) Has been cancelled
perf(publication): share frozen outcome metadata internally
Keep candidate-owned target mount IDs, operation counts and outcome metadata
shared through publication. Public report snapshots still defensively copy
values, while rollback journals and all precommit/shadow validators remain
unchanged. This removes repeated property graph copies from the publication
hot path without creating a second authority.

Validation: make -C tp check passed (120 public cases and API boundary check).
Integrated workspace checks passed. Added a metadata-only update/rollback
regression covering output, revision, client state and mount identity.
2026-09-18 07:21:37 +08:00
.githooks chore(git): enforce logical commits and structured messages 2026-09-13 11:15:12 +08:00
.github/workflows refactor(api)!: expose the root entry and enforce public contracts 2026-09-13 12:55:18 +08:00
benchmarks refactor(layout)!: place runtime sources in lisp 2026-09-13 11:28:45 +08:00
docs perf(publication): share frozen outcome metadata internally 2026-09-18 07:21:37 +08:00
examples chore: migrate conditional bindings for Emacs 31 2026-08-26 00:09:44 +08:00
lisp perf(publication): share frozen outcome metadata internally 2026-09-18 07:21:37 +08:00
scripts chore(api): register the etaf-db package entry 2026-09-15 11:27:51 +08:00
tests perf(publication): share frozen outcome metadata internally 2026-09-18 07:21:37 +08:00
.editorconfig chore: establish consistent repository and acceptance policy 2026-09-13 11:10:48 +08:00
.gitignore chore: establish consistent repository and acceptance policy 2026-09-13 11:10:48 +08:00
CHANGELOG.md perf(publication): share frozen outcome metadata internally 2026-09-18 07:21:37 +08:00
LICENSE Keep license and README files in package root 2026-09-13 10:02:03 +08:00
Makefile test(api)!: retain only public contract acceptance 2026-09-13 13:28:19 +08:00
README.md chore(policy): use scoped workspace development instructions 2026-09-13 23:34:25 +08:00
README.zh-CN.md chore(policy): use scoped workspace development instructions 2026-09-13 23:34:25 +08:00
tp.el refactor(api)!: expose the root entry and enforce public contracts 2026-09-13 12:55:18 +08:00

TP

Source layout: add the repository root to load-path and use (require 'tp). The supported API and usage notes are in tp.el; implementation modules live in lisp/.

TP 2.0 is a standalone retained/reactive text runtime for Emacs. It projects declarative properties, reactive data, and stable text objects onto strings and buffers while owning text-property composition, exact dependency tracking, retained identity, marker-backed mounts, diffing, transactions, rollback, and final buffer publication.

TP does not depend on Ebox or ECSS. It does not implement CSS selectors, stylesheets, specificity, cascade winners, Box, Flex, Grid, measurement, or layout. A CSS consumer may compute final declarations with ECSS and publish them through TP, but TP itself only understands Emacs text properties and generic retained text surfaces.

Chinese documentation: README.zh-CN.md.

Complete public API reference: API-REFERENCE.md (中文). It is the symbol-level usage index for the current TP 2.0 implementation; this README remains the conceptual quick start.

Requirements

  • Emacs 28.1 or newer.
  • No third-party runtime dependency.
(add-to-list 'load-path "/path/to/tp")
(require 'tp)

Choose the smallest public entry point

Need API Live runtime?
Return a propertized string tp-propertize No
Apply declarations once to an existing range tp-apply No
Reactively decorate existing host text tp-watch Yes, properties capability
Own retained text content tp-surface-mount / tp-surface-update Yes, content capability
Inspect or remove a retained publication tp-surface-report / tp-surface-inspect / tp-surface-unmount Yes

The one-shot and retained APIs use the same property-policy and projection semantics. One-shot calls deliberately create no object, binding, marker, subscription, or surface state.

Static properties

(let* ((callback (lambda (_window _object _position) "Open"))
       (text
        (tp-propertize
         "Hello"
         (list 'face '(:foreground "white" :background "navy")
               'help-echo callback
               'keymap nil))))
  text)

Explicit nil and an absent property are different. In the example above, keymap is present with value nil. Function values are literal data, so TP preserves callback instead of calling it.

To modify an existing range without changing its text:

(with-current-buffer (get-buffer-create "*tp-demo*")
  (erase-buffer)
  (insert "abcdef")
  (tp-apply (current-buffer) 2 5 '(face italic)))

The established tp-set, tp-reset, tp-add, tp-remove, tp-clear, tp-get, tp-at, tp-member, match, regexp, search, navigation, interval, and native query APIs remain the direct façade for ordinary text-property work.

Reusable declaration recipes

define-tp and define-tps define reusable recipes that expand to direct Emacs properties. They are definition-time conveniences, not live mounted layers, and they never write identity or provenance into displayed text.

(define-tp link-style (foreground)
  `(face (:foreground ,foreground :weight bold)
    mouse-face highlight
    help-echo "Open item"))

(tp-set "Project" '(link-style "#58a6ff"))

Use tp-computed when a property value must be evaluated. Every ordinary function object remains literal.

(defvar my-height 1.2)

(define-tp sized-label ()
  `(face ,(tp-computed (lambda () (list :height my-height)))))

The computed function runs in the current binding/prepare context, so tp-signal-read and tp-binding-read establish exact dependencies. Its returned value is normalized and projected once, then treated as literal data.

Reactive existing text

tp-watch decorates a marker-backed host range without owning or replacing its text:

(let ((online (tp-signal-create nil)))
  (with-current-buffer (get-buffer-create "*tp-status*")
    (erase-buffer)
    (insert "offline")
    (tp-watch
     (current-buffer) 1 8
     (lambda ()
       (list 'face
             (list :foreground
                   (if (tp-signal-read online) "green" "red")))))))

A signal records the binding that actually reads it. Updating one signal invalidates only its subscribers; conditional computations automatically stop subscribing to sources that the new branch no longer reads. Equal writes are no-ops, and repeated writes inside tp-with-transaction recompute each affected binding at most once.

tp-watch returns the underlying surface handle. Pass it to tp-surface-inspect, tp-surface-report, or tp-surface-unmount.

Retained content

A retained producer receives a prepare context, ensures stable objects before producing output, installs any object-local bindings, and returns a pure surface plan:

(let* ((status (tp-signal-create "ready"))
       (producer
        (lambda (context)
          (let* ((object (tp-object-ensure context nil 'status 'text))
                 (binding
                  (tp-bind object '(demo . status)
                           (lambda () (tp-signal-read status)))))
            (tp-surface-plan-create
             :key 'status
             :kind 'text
             :text (tp-binding-read binding)
             :props '(face bold)
             :capability 'content))))
       (buffer (get-buffer-create "*tp-retained*"))
       (surface
        (tp-surface-mount buffer producer '(:capability content))))
  (tp-signal-set status "done")
  (tp-surface-report surface))

Plan fields are key, kind, text, props, children, tags, and capability. Plans contain no markers, buffer positions, patch operations, producer closures, or client continuations. Normal constructors defensively copy caller-owned strings and property data; candidate-local producers may use the explicit -owned plan/result constructors when they transfer every nested value and stop exposing it. An owned result must be created for the active prepare context and is consumed once.

Stable identity is surface-local. tp-object-ensure reconciles by parent, sibling key, and kind; tp-object-resolve returns a live opaque handle by key path. tp-surface-update-scoped authorizes a full candidate update against one or more retained objects and rejects output changes outside their current mount ranges unless the caller explicitly selects root fallback.

tp-surface-materialize-string uses the same producer and plan semantics without creating a live surface. Its candidate objects, bindings, subscriptions, and anchors are released before it returns.

Host ranges and property ownership

The properties capability uses opaque range anchors. A producer creates or receives a tp-range-anchor-create handle and attaches it to an object with tp-object-attach-range. Markers follow host edits; the plan itself remains position-free.

TP records the host baseline and each TP contribution per property interval. Overlapping contributions are composed through the registered property policy. If external code changes a property after TP publishes it, the next update reports tp-property-conflict instead of overwriting the external value. tp-range-rebase explicitly accepts the current host value as the new baseline. Unmount restores only values still owned by TP and preserves conflicting host edits.

Transactions and failures

tp-with-transaction batches signal writes and all affected surfaces. TP prepares every candidate first, then publishes surfaces in stable order. A compute, validation, buffer write, marker/index publication, or transaction participant failure restores signal values, binding values and dependencies, text, properties, markers, indexes, plans, client state, revisions, and previous reports together.

Observers run only after a successful commit. Their failures are recorded and do not roll back an already committed transaction. A buffer killed during publication remains killed; rollback never recreates it.

TP 2.0 drives the single live publication from exact transaction-scoped batch entries, one frozen participant vector, and the candidate-bound final accept. The same journals, surface snapshots, and change group are shared rather than copied. Transaction participants register through tp-transaction-participate-v2; TP has no alternate transaction writer or runtime route switch. Generic opaque authority markers are bounded and whitelist-validated before final accept, then reverse-restored before ordinary rollback on partial apply or accept failure. tp-with-transaction still returns its body value, and internal outcomes remain observational side-channel evidence.

ETAF integration uses the same boundary through one opaque transaction participant. ETAF prepares its immutable generation and Ebox candidate before TP accepts the transaction; participant publish, TP final accept, and post-accept cleanup are ordered explicitly. A participant failure restores the previous generation and client state, while a post-accept observer failure is contained as a diagnostic. ETAF's runtime fixed-point guard records each effect's input/version tuple for one flush and stops repeated or over-bound cycles instead of spinning.

Property policies

tp-define-property-policy registers generic semantics for a final Emacs text property:

  • normalization and validation;
  • equality for no-op detection;
  • contribution merging;
  • projection to the final property value;
  • explicit presence, including present nil.

tp-register-text-property supplies the default policy for a native property. face contributions use merge semantics; other properties use their registered policy. tp-merge-declarations, tp-define-style, and tp-style-declarations operate only on direct declarations. They do not implement CSS cascade.

Public runtime families

Family Main public APIs
Core inspection and debug tp-debug-*, tp-intervals, tp-intervals-map, tp-plist, tp-text-snapshot, tp-empty-p
Property policy and declarations tp-define-property-policy, tp-register-text-property, tp-text-declarations, tp-computed, tp-resolve-value, tp-merge-declarations, tp-define-style, tp-style-declarations
Static recipes define-tp/tp-define-layer, define-tps/define-tp-group/tp-define-group, layer/group queries, undefine/reset/describe
Signals and bindings signal create/read/peek/set/dispose, binding install/read/dispose, tp-variable-signal, tp-with-transaction, tp-transaction-participate-v2, read-only tp-transaction-active-p, tp-runtime-manifest, counters/reset
Objects and plans plan/result constructors, tp-object-ensure, retain/reuse, fragment/content-range attachment, resolve, mounted/mounts
Host ranges tp-range-anchor-create, tp-range-anchor-live-p, tp-object-attach-range, tp-range-rebase
Surfaces mount/update/scoped update, materialize, live/revision/client-state, at-point, report/report-summary/inspect, unmount
Direct façade tp-propertize, tp-apply, tp-watch, tp-set, tp-reset, tp-add, tp-remove, tp-clear, tp-get, tp-at, tp-member
Search and navigation tp-match-*, tp-regexp-*, tp-search, tp-search-map, tp-forward*, tp-backward*
Native query and mutation tp-lookup, tp-property-change, tp-property-any, tp-property-not-all, tp-with-mutation-policy
Palette and display helpers palette definition/lookups, built-in recipes, tp-palette-show, tp-pop-to-buffer, tp-switch-to-buffer, tp-display-buffer-mode

See the complete API reference for signatures, return shapes, examples, and the full module index. See API semantics for ownership, lifecycle, error, and return contracts, and architecture for module boundaries and transaction flow.

TP 1.0 migration

TP 1.0 removes the 0.3 managed stack/renderer runtime instead of hiding it behind compatibility branches. Removed behavior includes tp-render.el, tp-stack.el, stack mutation APIs, tp-text, $variable declarations, layer-to-buffer registries, scan-driven refresh, managed attach/detach/diagnostics, and inline tp-name/tp-layers/tp-meta runtime storage.

TP 2.0 transaction migration

TP 2.0 removes the v1 transaction participant facade and the legacy execution route. Replace (tp-transaction-participate KEY PUBLISH ROLLBACK) exactly with (tp-transaction-participate-v2 :key KEY :stage PUBLISH :rollback ROLLBACK). Callers that inspect tp-runtime-manifest must require tp-transaction-protocol-v2; route-selection and v1-adapter fields are no longer published.

Use direct recipes for reusable static declarations, tp-watch for reactive properties on existing text, and retained content surfaces for reactive text or structured UI. TP does not automatically scan historical propertized text to reconstruct runtime identity.

Examples

These examples use only TP public APIs and are tested without Ebox or ECSS on the load path.

Verification

make test
make doctest
make compile-all WERROR=t
make checkdoc
make package-lint
make diff-check

The structural tests also prove that normal signal and surface updates do not scan buffer-list or search displayed text for identity, equal results do not publish a new revision, and unmount/kill cleanup releases subscriptions and marker-backed runtime state.

License

GPL-3.0-or-later. See LICENSE.

Benchmarking

This document describes the benchmark runner shipped with TP 1.0. It measures the current retained/reactive runtime; it is not a historical TP 0.3 stack benchmark and it does not impose a release threshold.

Run

Use the Makefile entry point:

make benchmark

The equivalent batch command is:

emacs -Q --batch -L lisp -l benchmarks/tp-benchmark.el -f tp-benchmark-run

The runner uses fixed seeds 1, 7, 42, 747555 and generated seed 8675309. Each scenario performs a correctness assertion before timing the operation. Record the Emacs version, machine, seed, and full output when comparing runs.

Scenarios

For every seed, the runner executes:

Scenario Fixtures
large-text strings of 100,000 and 1,000,000 characters; set and presence-aware search
fragmented 1,000, 10,000, and 50,000 alternating property intervals
retained-keyed-reconcile retained content with 10, 100, and 1,000 stable keyed entries
signal-sparse-update one target binding beside 1, 100, and 10,000 unrelated bindings
transaction-batch 1, 100, and 10,000 writes to one retained surface
equal-write-noop the same write counts, all equal to the committed value

The retained scenarios verify stable object reconciliation, correct published text, dependency-local recomputation, one publication for a batch, and no revision change for equal writes. A failed assertion aborts the run instead of producing misleading timing evidence.

Output

Each row is a whitespace-separated key/value record. The stable fields are:

scenario, status, fixture, seed, requested, actual, operations, objects, subscribers, invalidated, recomputed, skipped, text-operations, property-operations, touched, revision, published, elapsed, gcs, and note.

The output is deliberately machine-readable enough for local comparison, but it is not a compatibility format. Interpret it together with the scenario source in tp-benchmark.el.

Interpretation

These measurements are advisory. Runtime, garbage collection, Emacs build, machine load, and buffer implementation details affect absolute timings. Compare like-for-like runs, inspect correctness failures first, and use the reports/counters to explain a regression:

  • fragmented measures expose interval-run scaling;
  • retained-keyed-reconcile measures keyed object reuse and publication work;
  • signal-sparse-update checks that unrelated bindings are not recomputed;
  • transaction-batch measures deduplicated recomputation and one surface commit;
  • equal-write-noop checks that equal values do not publish a new revision.

For the contracts behind these scenarios, read API semantics, architecture, and the public API reference.

Development

After cloning, run make setup-hooks. Before submitting a change, run make check; make structure-check is the fast organization gate.

CHANGELOG.md

make check runs structure checks, compilation and the public acceptance scenarios listed in tests/acceptance.json. make test runs the same public API suite. The inventory covers every maintained Lisp test. GUI and performance checks use separate targets.

Source layout

Add the repository root to load-path and require tp. The root entry loads the implementation in lisp/ and documents the supported public APIs in its Commentary. Package archives include both the root entry and lisp/.