refactor: remove manual app precompilation
This commit is contained in:
parent
f69f6889ff
commit
459098b074
60
DESIGN.md
60
DESIGN.md
@ -122,66 +122,6 @@ layout, scans no ancestor slot, and walks no complete fragment ledger. Real
|
|||||||
text or structure changes use a separate content/layout transaction; the
|
text or structure changes use a separate content/layout transaction; the
|
||||||
parent ETAF operation still correlates and atomically commits both.
|
parent ETAF operation still correlates and atomically commits both.
|
||||||
|
|
||||||
## Precompiled Operation Programs and performance composability
|
|
||||||
|
|
||||||
A compiled artifact cannot retain only View syntax or final text coordinates.
|
|
||||||
An Operation Program that the Runtime can consume directly owns five facts as
|
|
||||||
one indivisible contract:
|
|
||||||
|
|
||||||
1. stable Host/Box/Range topology templates and dynamic holes;
|
|
||||||
2. `source/effect → OperationBatch` dependency routes;
|
|
||||||
3. layout slots, containment, overflow, and revision invalidation edges;
|
|
||||||
4. a closed style environment covering selectors, inheritance, and Theme
|
|
||||||
contributions;
|
|
||||||
5. TP object/ownership ranges/paint contribution addresses plus atomic
|
|
||||||
rollback boundaries.
|
|
||||||
|
|
||||||
Retaining only a subset merely moves the same work downstream. Coordinates
|
|
||||||
without style and TP objects require reconstruction; a View blueprint still
|
|
||||||
requires lowering, layout, and paint; flattened final faces discard TP
|
|
||||||
layering. Such partial artifacts are not App performance compilation.
|
|
||||||
|
|
||||||
The Runtime submits only precise operation batches:
|
|
||||||
|
|
||||||
```text
|
|
||||||
OperationBatch = RangeReplace | HostPropertySet | InlineTextSet |
|
|
||||||
LayoutInvalidate | PaintContributionSet
|
|
||||||
```
|
|
||||||
|
|
||||||
Each layer consumes only its fields and forwards the remaining retained
|
|
||||||
addresses. Data materializes once; a transaction performs at most one full
|
|
||||||
walk; every module costs `O(changed)` or a documented small constant. If an
|
|
||||||
extra module materially increases total latency, its interface discarded an
|
|
||||||
upstream intermediate—the overhead is not an inherent cost of architecture.
|
|
||||||
|
|
||||||
The current `etaf-view-blueprint/0` caches only static View construction and
|
|
||||||
is a migration artifact. A later ABI becomes an Operation Program only after
|
|
||||||
a real App Range update avoids whole-tree Ebox projection/render and passes
|
|
||||||
the formal Pagination budget.
|
|
||||||
|
|
||||||
### Root implementation order and acceptance
|
|
||||||
|
|
||||||
1. Theme vertical slice: add a Theme source→Host/property index and TP
|
|
||||||
contributions. Color-consuming Component render count becomes zero while
|
|
||||||
content consumers update separately.
|
|
||||||
2. Generic property effects: compile reactive expressions written directly in
|
|
||||||
Host property positions into stable property bindings; structural
|
|
||||||
expressions remain Component/Range work.
|
|
||||||
3. Exact incremental layout: Ebox retains property-impact and containment
|
|
||||||
dependency edges, recomputes the dirty closure directly, and removes
|
|
||||||
span/ancestor proofs from the normal path.
|
|
||||||
4. TP paint plane: migrate precomposed fragment faces to ordered properties
|
|
||||||
contributions and delete full-fragment palette recomposition.
|
|
||||||
5. Converge: remove compatibility caches and proofs that are superseded while
|
|
||||||
retaining the conservative root path for arbitrary candidates, rollback,
|
|
||||||
and all existing semantics.
|
|
||||||
|
|
||||||
Acceptance requires GUI-visible completion timing, not batch alone; no layout
|
|
||||||
stage or full-fragment recomposition for Theme paint; no regressions in
|
|
||||||
structure, slots, lifecycle, inheritance, precedence, conflicts, or rollback;
|
|
||||||
and representative warmed operations passing the scenario budgets in the
|
|
||||||
performance evaluator.
|
|
||||||
|
|
||||||
## Visual language
|
## Visual language
|
||||||
- Color: Warm canvas `#F8F5EE`, paper `#FFFDF8`, ink `#252A2E`, muted ink `#66706A`, terracotta `#F1D4C9`, sage `#DCEBDD`, blue `#D9EAF2`, and violet `#E7E2F1`, always with explicit dark text.
|
- Color: Warm canvas `#F8F5EE`, paper `#FFFDF8`, ink `#252A2E`, muted ink `#66706A`, terracotta `#F1D4C9`, sage `#DCEBDD`, blue `#D9EAF2`, and violet `#E7E2F1`, always with explicit dark text.
|
||||||
- Typography: The configured Emacs monospace face; bold only for titles, actions, statuses, and important values.
|
- Typography: The configured Emacs monospace face; bold only for titles, actions, statuses, and important values.
|
||||||
|
|||||||
@ -107,54 +107,6 @@ Theme source changed
|
|||||||
slot,或遍历完整 fragment ledger。只有真实文本/结构变化走独立的 content/layout
|
slot,或遍历完整 fragment ledger。只有真实文本/结构变化走独立的 content/layout
|
||||||
事务;二者仍由同一个上层 ETAF operation 关联并原子提交。
|
事务;二者仍由同一个上层 ETAF operation 关联并原子提交。
|
||||||
|
|
||||||
## 预编译 Operation Program 与性能可组合性
|
|
||||||
|
|
||||||
预编译产物不能只保存 View 语法树或最终文本坐标。一个可以在运行时直接消费的
|
|
||||||
Operation Program 必须把下面五项作为同一个不可拆分契约:
|
|
||||||
|
|
||||||
1. 稳定 Host/Box/Range 拓扑模板和动态 hole;
|
|
||||||
2. `source/effect → OperationBatch` 的依赖路由;
|
|
||||||
3. 布局 slot、containment、overflow 与 revision 失效边;
|
|
||||||
4. 已闭合的样式计算环境,包括 selector/继承/Theme contribution;
|
|
||||||
5. TP object/ownership range/paint contribution 地址和原子回滚边界。
|
|
||||||
|
|
||||||
只保留其中一部分会把工作转移到下一层:只保留坐标仍需重新建立 style 与 TP
|
|
||||||
object;只保留 View blueprint 仍需重新 lower、layout 和 paint;只保留最终 face
|
|
||||||
又会丢失 TP 分层。因此这种“局部预编译”不能宣称为 App 性能预编译。
|
|
||||||
|
|
||||||
运行时只允许提交精确操作批次:
|
|
||||||
|
|
||||||
```text
|
|
||||||
OperationBatch = RangeReplace | HostPropertySet | InlineTextSet |
|
|
||||||
LayoutInvalidate | PaintContributionSet
|
|
||||||
```
|
|
||||||
|
|
||||||
每一层只消费属于自己的字段并保留其余地址。一次数据只 materialize 一次;同一
|
|
||||||
事务的全量遍历最多一次;各模块成本必须是 `O(changed)` 或有明确小常数上界。
|
|
||||||
如果增加一个模块后总耗时明显增加,说明接口丢失了上层中间产物,而不是架构
|
|
||||||
层数本身应有的代价。
|
|
||||||
|
|
||||||
当前 `etaf-view-blueprint/0` 只缓存静态 View 构造,是迁移产物。下一 ABI 只有在
|
|
||||||
真实 App 的 Range 操作无需重新进行整树 Ebox projection/render,并通过正式
|
|
||||||
Pagination 预算后,才能称为 Operation Program。
|
|
||||||
|
|
||||||
### 根源实现顺序与验收
|
|
||||||
|
|
||||||
1. Theme 垂直切片:建立 Theme source→Host/property 索引和 TP contribution;
|
|
||||||
Theme 颜色消费者的 Component render 次数为 0,内容消费者单独更新。
|
|
||||||
2. 通用 property effect:把直接写在 Host 属性位置的响应式表达式编译为稳定
|
|
||||||
property binding;结构表达式继续归 Component/Range。
|
|
||||||
3. 精确 incremental layout:Ebox 持有 property impact 与 containment 依赖图,
|
|
||||||
直接重算 dirty closure,移除正常路径的 span/ancestor proof。
|
|
||||||
4. TP paint plane:将预合成 fragment face 迁移为有序 properties contribution,
|
|
||||||
删除 palette 切换时的全 fragment recomposition。
|
|
||||||
5. 收尾:删除已被替代的兼容缓存和 proof;保守 root 路径继续保证任意 candidate、
|
|
||||||
rollback 和现有功能语义。
|
|
||||||
|
|
||||||
验收必须同时满足:GUI 可见完成计时而非只看 batch;Theme paint 操作没有 layout
|
|
||||||
阶段和全 fragment 重组;结构、slot、lifecycle、继承、优先级、冲突与 rollback
|
|
||||||
测试不退化;代表性 warmed 操作达到性能 evaluator 的场景预算。
|
|
||||||
|
|
||||||
## 视觉语言
|
## 视觉语言
|
||||||
- 颜色:暖色画布 `#F8F5EE`、纸张 `#FFFDF8`、正文 `#252A2E`、弱化正文 `#66706A`、陶土色 `#F1D4C9`、鼠尾草绿 `#DCEBDD`、蓝色 `#D9EAF2` 与紫色 `#E7E2F1`,始终搭配明确的深色文字。
|
- 颜色:暖色画布 `#F8F5EE`、纸张 `#FFFDF8`、正文 `#252A2E`、弱化正文 `#66706A`、陶土色 `#F1D4C9`、鼠尾草绿 `#DCEBDD`、蓝色 `#D9EAF2` 与紫色 `#E7E2F1`,始终搭配明确的深色文字。
|
||||||
- 字体:沿用 Emacs 当前等宽字体;粗体只用于标题、操作、状态和关键数值。
|
- 字体:沿用 Emacs 当前等宽字体;粗体只用于标题、操作、状态和关键数值。
|
||||||
|
|||||||
4
Makefile
4
Makefile
@ -1,8 +1,8 @@
|
|||||||
EMACS ?= emacs
|
EMACS ?= emacs
|
||||||
LOAD_PATH = -L . -L examples -L ../ebox -L ../tp -L ../ecss
|
LOAD_PATH = -L . -L examples -L ../ebox -L ../tp -L ../ecss
|
||||||
SOURCES = etaf-view.el etaf-compiler.el etaf-app.el etaf-component.el etaf-reactive.el etaf-context.el etaf-theme-tp.el etaf-resource.el etaf-data.el etaf-renderer.el etaf-runtime.el etaf-behavior.el etaf-actions.el etaf-events.el etaf-performance.el etaf.el
|
SOURCES = etaf-view.el etaf-compiler.el etaf-component.el etaf-reactive.el etaf-context.el etaf-theme-tp.el etaf-resource.el etaf-data.el etaf-renderer.el etaf-runtime.el etaf-behavior.el etaf-actions.el etaf-events.el etaf-performance.el etaf.el
|
||||||
EXAMPLES = examples/etaf-counter-example.el examples/etaf-data-example.el examples/etaf-resource-example.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-app-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-performance-tests.el
|
TESTS = tests/etaf-tests.el tests/etaf-compiler-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-performance-tests.el
|
||||||
|
|
||||||
.PHONY: test compile load checkdoc docs-check check clean
|
.PHONY: test compile load checkdoc docs-check check clean
|
||||||
|
|
||||||
|
|||||||
31
README.md
31
README.md
@ -40,37 +40,6 @@ Define a Component:
|
|||||||
|
|
||||||
`etaf-view` is the single public View constructor. Structural forms do not use quote; quote remains ordinary Elisp data syntax, such as `'bold`. A View returned from ordinary Elisp is explicitly constructed with `(etaf-view ...)` inside `expr`.
|
`etaf-view` is the single public View constructor. Structural forms do not use quote; quote remains ordinary Elisp data syntax, such as `'bold`. A View returned from ordinary Elisp is explicitly constructed with `(etaf-view ...)` inside `expr`.
|
||||||
|
|
||||||
## App precompilation
|
|
||||||
|
|
||||||
App precompilation is an ETAF core capability and does not depend on
|
|
||||||
Playground. An App or host first registers its source manifest and artifact
|
|
||||||
location:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(etaf-register-app
|
|
||||||
"my-app"
|
|
||||||
:source "/path/to/my-app.el"
|
|
||||||
:static "/path/to/my-app.etaf"
|
|
||||||
:style "/path/to/my-app.ecss"
|
|
||||||
:artifact "/path/to/build/my-app.etafc")
|
|
||||||
```
|
|
||||||
|
|
||||||
The user then runs `M-x etaf-compile-app` once to select a registered App and
|
|
||||||
atomically generate its standalone `.etafc` intermediate artifact. Use
|
|
||||||
`M-x etaf-app-compile-status` to report `current`, `missing`, `stale`, or
|
|
||||||
`invalid`.
|
|
||||||
|
|
||||||
ETAF records content hashes for the companion, `.etaf`, and `.ecss` sources;
|
|
||||||
changing any of them rejects the old artifact. An App host calls
|
|
||||||
`etaf-app-load-artifact-or-warn` before startup. A current artifact is loaded;
|
|
||||||
a missing or stale artifact retains the complete source fallback and emits an
|
|
||||||
explicit reminder to run `M-x etaf-compile-app`.
|
|
||||||
|
|
||||||
`etaf-compiler.el` owns blueprint and `.etafc` compilation/validation.
|
|
||||||
`etaf-app.el` independently owns App registration, interactive commands,
|
|
||||||
status, and fallback warnings. Playground is only an optional UI consumer of
|
|
||||||
these core APIs.
|
|
||||||
|
|
||||||
## Performance records
|
## Performance records
|
||||||
|
|
||||||
ETAF includes an opt-in, application-neutral timing recorder. It recognizes
|
ETAF includes an opt-in, application-neutral timing recorder. It recognizes
|
||||||
|
|||||||
@ -40,41 +40,6 @@ Component(props, Scope) → View → Renderer → Ebox Node → Emacs buffer
|
|||||||
|
|
||||||
`etaf-view` 是唯一的公共 View 构造入口。结构 form 不使用 quote;quote 仍然是普通 Elisp 数据语法,例如 `'bold`。普通 Elisp 返回 View 时,必须在 `expr` 中显式使用 `(etaf-view ...)` 构造它。
|
`etaf-view` 是唯一的公共 View 构造入口。结构 form 不使用 quote;quote 仍然是普通 Elisp 数据语法,例如 `'bold`。普通 Elisp 返回 View 时,必须在 `expr` 中显式使用 `(etaf-view ...)` 构造它。
|
||||||
|
|
||||||
## App 预编译
|
|
||||||
|
|
||||||
App 预编译是 ETAF core 的通用能力,不依赖 Playground。App 或宿主先注册文件
|
|
||||||
清单和产物位置:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(etaf-register-app
|
|
||||||
"my-app"
|
|
||||||
:source "/path/to/my-app.el"
|
|
||||||
:static "/path/to/my-app.etaf"
|
|
||||||
:style "/path/to/my-app.ecss"
|
|
||||||
:artifact "/path/to/build/my-app.etafc")
|
|
||||||
```
|
|
||||||
|
|
||||||
用户手动执行一次:
|
|
||||||
|
|
||||||
```text
|
|
||||||
M-x etaf-compile-app
|
|
||||||
```
|
|
||||||
|
|
||||||
命令会选择已注册 App,并原子生成独立 `.etafc` 中间产物。查看状态使用:
|
|
||||||
|
|
||||||
```text
|
|
||||||
M-x etaf-app-compile-status
|
|
||||||
```
|
|
||||||
|
|
||||||
状态包括 `current`、`missing`、`stale` 和 `invalid`。ETAF 会记录 companion、
|
|
||||||
`.etaf` 和 `.ecss` 的内容 hash;任一源码变化都会拒绝旧产物。宿主启动 App 时
|
|
||||||
调用 `etaf-app-load-artifact-or-warn`:有效产物直接加载,缺失或过期则保持完整
|
|
||||||
source fallback 功能,同时明确提醒用户运行 `M-x etaf-compile-app`。
|
|
||||||
|
|
||||||
`etaf-compiler.el` 只负责 blueprint 与 `.etafc` 的编译/校验;App 注册、交互
|
|
||||||
命令、状态和回退提醒属于独立的 `etaf-app.el`。Playground 只是这个通用 API 的
|
|
||||||
一个可选 UI consumer。
|
|
||||||
|
|
||||||
## 性能记录面板
|
## 性能记录面板
|
||||||
|
|
||||||
ETAF 内置了一个按需启用、与具体应用无关的性能记录器。它自动识别公共
|
ETAF 内置了一个按需启用、与具体应用无关的性能记录器。它自动识别公共
|
||||||
|
|||||||
204
etaf-app.el
204
etaf-app.el
@ -1,204 +0,0 @@
|
|||||||
;;; etaf-app.el --- ETAF App registry and compile workflow -*- lexical-binding: t; -*-
|
|
||||||
|
|
||||||
;; SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
;;; Commentary:
|
|
||||||
|
|
||||||
;; This module owns the user-facing App boundary. The compiler remains a pure
|
|
||||||
;; engine; hosts such as etaf-playground register source manifests here and use
|
|
||||||
;; the same compile/status/fallback workflow as any other ETAF application.
|
|
||||||
|
|
||||||
;;; Code:
|
|
||||||
|
|
||||||
(require 'cl-lib)
|
|
||||||
(require 'etaf-compiler)
|
|
||||||
|
|
||||||
(cl-defstruct (etaf-app-spec (:constructor etaf-app--spec-create))
|
|
||||||
name source-file static-file style-file artifact-file)
|
|
||||||
|
|
||||||
(defvar etaf-app--registry (make-hash-table :test #'equal))
|
|
||||||
(defvar etaf-app--warning-state (make-hash-table :test #'equal))
|
|
||||||
(defvar etaf-app--status-cache (make-hash-table :test #'equal))
|
|
||||||
|
|
||||||
(declare-function ebox-native-reflow-load "ebox-native-reflow" ())
|
|
||||||
(declare-function ebox-native-reflow-layout-ready-p "ebox-native-reflow" ())
|
|
||||||
|
|
||||||
(defun etaf-app--prepare-runtime-accelerator ()
|
|
||||||
"Load the optional compiled Ebox runtime and return its status."
|
|
||||||
(if (not (require 'ebox-native-reflow nil t))
|
|
||||||
'unavailable
|
|
||||||
(condition-case _error
|
|
||||||
(progn
|
|
||||||
(ebox-native-reflow-load)
|
|
||||||
(if (ebox-native-reflow-layout-ready-p) 'ready 'unavailable))
|
|
||||||
(error 'unavailable))))
|
|
||||||
|
|
||||||
(defcustom etaf-app-warn-on-artifact-fallback t
|
|
||||||
"Whether running an uncompiled or stale App emits a warning."
|
|
||||||
:type 'boolean
|
|
||||||
:group 'etaf)
|
|
||||||
|
|
||||||
;;;###autoload
|
|
||||||
(cl-defun etaf-register-app
|
|
||||||
(name &key source static style artifact)
|
|
||||||
"Register App NAME and its SOURCE, STATIC, STYLE, and ARTIFACT files."
|
|
||||||
(setq name (if (symbolp name) (symbol-name name) name))
|
|
||||||
(unless (and (stringp name) (stringp source) (stringp static)
|
|
||||||
(stringp artifact))
|
|
||||||
(signal 'wrong-type-argument
|
|
||||||
(list 'etaf-app-manifest name source static style artifact)))
|
|
||||||
(let ((spec (etaf-app--spec-create
|
|
||||||
:name name
|
|
||||||
:source-file (expand-file-name source)
|
|
||||||
:static-file (expand-file-name static)
|
|
||||||
:style-file (and style (expand-file-name style))
|
|
||||||
:artifact-file (expand-file-name artifact))))
|
|
||||||
(unless (equal spec (gethash name etaf-app--registry))
|
|
||||||
(remhash name etaf-app--status-cache))
|
|
||||||
(puthash name spec etaf-app--registry)
|
|
||||||
spec))
|
|
||||||
|
|
||||||
(defun etaf-app-names ()
|
|
||||||
"Return registered App names in stable order."
|
|
||||||
(let (names)
|
|
||||||
(maphash (lambda (name _spec) (push name names)) etaf-app--registry)
|
|
||||||
(sort names #'string<)))
|
|
||||||
|
|
||||||
(defun etaf-app-spec (name)
|
|
||||||
"Return registered App NAME or signal a user error."
|
|
||||||
(setq name (if (symbolp name) (symbol-name name) name))
|
|
||||||
(or (gethash name etaf-app--registry)
|
|
||||||
(user-error "Unknown ETAF App: %s" name)))
|
|
||||||
|
|
||||||
(defun etaf-app--interactive-name (prompt)
|
|
||||||
"Read a registered App name using PROMPT."
|
|
||||||
(let ((names (etaf-app-names)))
|
|
||||||
(unless names (user-error "No ETAF Apps are registered"))
|
|
||||||
(completing-read prompt names nil t nil nil (car names))))
|
|
||||||
|
|
||||||
(defun etaf-app--file-fingerprint (file)
|
|
||||||
"Return a cheap invalidation fingerprint for FILE."
|
|
||||||
(when-let ((attributes (and file (file-attributes file 'string))))
|
|
||||||
(list file
|
|
||||||
(file-attribute-size attributes)
|
|
||||||
(file-attribute-modification-time attributes)
|
|
||||||
(file-attribute-inode-number attributes))))
|
|
||||||
|
|
||||||
(defun etaf-app--spec-fingerprint (spec)
|
|
||||||
"Return source/artifact invalidation facts for SPEC."
|
|
||||||
(mapcar #'etaf-app--file-fingerprint
|
|
||||||
(list (etaf-app-spec-source-file spec)
|
|
||||||
(etaf-app-spec-static-file spec)
|
|
||||||
(etaf-app-spec-style-file spec)
|
|
||||||
(etaf-app-spec-artifact-file spec))))
|
|
||||||
|
|
||||||
(defun etaf-app--status (name &optional use-cache)
|
|
||||||
"Return App NAME's compilation status plist.
|
|
||||||
USE-CACHE reuses a fully validated result while file fingerprints are stable."
|
|
||||||
(let* ((spec (etaf-app-spec name))
|
|
||||||
(file (etaf-app-spec-artifact-file spec))
|
|
||||||
(fingerprint (etaf-app--spec-fingerprint spec))
|
|
||||||
(cached (and use-cache (gethash name etaf-app--status-cache)))
|
|
||||||
(source-directory
|
|
||||||
(file-name-directory (etaf-app-spec-source-file spec))))
|
|
||||||
(if (and cached (equal fingerprint (car cached)))
|
|
||||||
(cdr cached)
|
|
||||||
(let ((status
|
|
||||||
(if (not (file-readable-p file))
|
|
||||||
(list :app (etaf-app-spec-name spec)
|
|
||||||
:status 'missing :file file)
|
|
||||||
(condition-case err
|
|
||||||
(let ((artifact
|
|
||||||
(etaf-load-app-artifact file source-directory)))
|
|
||||||
(list :app (etaf-app-spec-name spec)
|
|
||||||
:status 'current :file file
|
|
||||||
:blueprints
|
|
||||||
(length (plist-get artifact :blueprints))
|
|
||||||
:artifact artifact))
|
|
||||||
(etaf-compiler-stale-artifact
|
|
||||||
(list :app (etaf-app-spec-name spec) :status 'stale
|
|
||||||
:file file :error err))
|
|
||||||
(etaf-compiler-artifact-error
|
|
||||||
(list :app (etaf-app-spec-name spec) :status 'invalid
|
|
||||||
:file file :error err))
|
|
||||||
(error
|
|
||||||
(list :app (etaf-app-spec-name spec) :status 'invalid
|
|
||||||
:file file :error err))))))
|
|
||||||
(puthash name (cons fingerprint status) etaf-app--status-cache)
|
|
||||||
(unless (eq (plist-get status :status) 'current)
|
|
||||||
(etaf-compiler-deactivate-app-artifact name))
|
|
||||||
status))))
|
|
||||||
|
|
||||||
;;;###autoload
|
|
||||||
(defun etaf-app-compile-status (&optional name)
|
|
||||||
"Show and return compilation status for App NAME."
|
|
||||||
(interactive (list (etaf-app--interactive-name "ETAF App status: ")))
|
|
||||||
(setq name (or name (car (etaf-app-names))))
|
|
||||||
(let* ((status (etaf-app--status name))
|
|
||||||
(state (plist-get status :status))
|
|
||||||
(detail
|
|
||||||
(pcase state
|
|
||||||
('current (format "compiled, current, %d blueprints"
|
|
||||||
(plist-get status :blueprints)))
|
|
||||||
('missing "not compiled")
|
|
||||||
('stale "compiled artifact is stale")
|
|
||||||
(_ "compiled artifact is invalid"))))
|
|
||||||
(when (called-interactively-p 'interactive)
|
|
||||||
(message "ETAF App %s: %s (%s)"
|
|
||||||
name detail (plist-get status :file)))
|
|
||||||
status))
|
|
||||||
|
|
||||||
;;;###autoload
|
|
||||||
(defun etaf-compile-app (&optional name)
|
|
||||||
"Compile registered App NAME into its standalone `.etafc' artifact."
|
|
||||||
(interactive (list (etaf-app--interactive-name "Compile ETAF App: ")))
|
|
||||||
(setq name (or name (car (etaf-app-names))))
|
|
||||||
(let* ((spec (etaf-app-spec name))
|
|
||||||
(output (etaf-app-spec-artifact-file spec)))
|
|
||||||
(etaf-compiler-write-app-artifact
|
|
||||||
:name (etaf-app-spec-name spec)
|
|
||||||
:source (etaf-app-spec-source-file spec)
|
|
||||||
:static (etaf-app-spec-static-file spec)
|
|
||||||
:style (etaf-app-spec-style-file spec)
|
|
||||||
:output output)
|
|
||||||
(remhash name etaf-app--warning-state)
|
|
||||||
(remhash name etaf-app--status-cache)
|
|
||||||
(let ((status (etaf-app--status name)))
|
|
||||||
(unless (eq (plist-get status :status) 'current)
|
|
||||||
(error "ETAF App compilation produced an unusable artifact: %S"
|
|
||||||
status))
|
|
||||||
(when (called-interactively-p 'interactive)
|
|
||||||
(message "Compiled ETAF App %s: %s (%d blueprints)"
|
|
||||||
name output (plist-get status :blueprints)))
|
|
||||||
(setq status
|
|
||||||
(plist-put (copy-sequence status) :runtime-accelerator
|
|
||||||
(etaf-app--prepare-runtime-accelerator)))
|
|
||||||
status)))
|
|
||||||
|
|
||||||
(defun etaf-app--warn-fallback (name status)
|
|
||||||
"Warn once that App NAME is using source fallback because of STATUS."
|
|
||||||
(when (and etaf-app-warn-on-artifact-fallback
|
|
||||||
(not noninteractive)
|
|
||||||
(not (eq (gethash name etaf-app--warning-state) status)))
|
|
||||||
(puthash name status etaf-app--warning-state)
|
|
||||||
(display-warning
|
|
||||||
'etaf
|
|
||||||
(format (concat "ETAF App %s is %s; running the slower source fallback. "
|
|
||||||
"Run M-x etaf-compile-app.")
|
|
||||||
name status)
|
|
||||||
:warning)))
|
|
||||||
|
|
||||||
(defun etaf-app-load-artifact-or-warn (name)
|
|
||||||
"Return App NAME's current artifact, or warn and return nil."
|
|
||||||
(let* ((status (etaf-app--status name t))
|
|
||||||
(state (plist-get status :status)))
|
|
||||||
(if (eq state 'current)
|
|
||||||
(progn
|
|
||||||
(etaf-app--prepare-runtime-accelerator)
|
|
||||||
(plist-get status :artifact))
|
|
||||||
(etaf-compiler-note-artifact-miss state)
|
|
||||||
(etaf-app--warn-fallback name state)
|
|
||||||
nil)))
|
|
||||||
|
|
||||||
(provide 'etaf-app)
|
|
||||||
;;; etaf-app.el ends here
|
|
||||||
237
etaf-compiler.el
237
etaf-compiler.el
@ -1,56 +1,27 @@
|
|||||||
;;; etaf-compiler.el --- AOT View blueprints for ETAF -*- lexical-binding: t; -*-
|
;;; etaf-compiler.el --- Automatic View blueprint lowering -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
;; SPDX-License-Identifier: GPL-3.0-or-later
|
;; SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
;;; Commentary:
|
;;; Commentary:
|
||||||
|
|
||||||
;; This is the first compiler slice: structured View topology and literal
|
;; `etaf-view' lowers supported structured topology into an embedded blueprint
|
||||||
;; values are emitted into an immutable blueprint during byte compilation.
|
;; as part of ordinary macro expansion. Runtime instantiation constructs only
|
||||||
;; Runtime instantiation constructs only nodes on paths containing dynamic
|
;; nodes on paths containing dynamic property holes. Unsupported structural
|
||||||
;; property holes. Unsupported structural forms use the existing View
|
;; forms use the existing View expansion unchanged. There is no standalone
|
||||||
;; compiler unchanged.
|
;; App artifact or user-managed precompilation workflow.
|
||||||
|
|
||||||
;;; Code:
|
;;; Code:
|
||||||
|
|
||||||
(require 'cl-lib)
|
(require 'cl-lib)
|
||||||
(require 'etaf-view)
|
(require 'etaf-view)
|
||||||
|
|
||||||
(defvar read-eval)
|
|
||||||
|
|
||||||
(defconst etaf-compiler-blueprint-abi "etaf-view-blueprint/1")
|
(defconst etaf-compiler-blueprint-abi "etaf-view-blueprint/1")
|
||||||
(defconst etaf-compiler-app-artifact-abi "etaf-app/3")
|
|
||||||
|
|
||||||
(define-error 'etaf-compiler-artifact-error "Invalid ETAF App artifact")
|
|
||||||
(define-error 'etaf-compiler-stale-artifact "Stale ETAF App artifact"
|
|
||||||
'etaf-compiler-artifact-error)
|
|
||||||
|
|
||||||
(defvar etaf-compiler--static-cache (make-hash-table :test #'equal))
|
(defvar etaf-compiler--static-cache (make-hash-table :test #'equal))
|
||||||
(defvar etaf-compiler--artifact-blueprints (make-hash-table :test #'equal))
|
|
||||||
(defvar etaf-compiler--artifact-blueprint-owners
|
|
||||||
(make-hash-table :test #'equal))
|
|
||||||
(defvar etaf-compiler--active-artifacts (make-hash-table :test #'equal))
|
|
||||||
(defvar etaf-compiler--registry-epoch 0)
|
(defvar etaf-compiler--registry-epoch 0)
|
||||||
(defvar etaf-compiler--instantiate-count 0)
|
(defvar etaf-compiler--instantiate-count 0)
|
||||||
(defvar etaf-compiler--fallback-count 0)
|
(defvar etaf-compiler--fallback-count 0)
|
||||||
(defvar etaf-compiler--last-blueprint nil)
|
(defvar etaf-compiler--last-blueprint nil)
|
||||||
(defvar etaf-compiler--artifact-hit-count 0)
|
|
||||||
(defvar etaf-compiler--artifact-miss-count 0)
|
|
||||||
(defvar etaf-compiler--last-artifact-status nil)
|
|
||||||
|
|
||||||
(defun etaf-compiler-deactivate-app-artifact (name)
|
|
||||||
"Remove App NAME's active artifact and unowned blueprints."
|
|
||||||
(when-let ((artifact (gethash name etaf-compiler--active-artifacts)))
|
|
||||||
(dolist (entry (plist-get artifact :blueprints))
|
|
||||||
(let* ((id (plist-get entry :id))
|
|
||||||
(owners (delete name
|
|
||||||
(copy-sequence
|
|
||||||
(gethash id
|
|
||||||
etaf-compiler--artifact-blueprint-owners)))))
|
|
||||||
(if owners
|
|
||||||
(puthash id owners etaf-compiler--artifact-blueprint-owners)
|
|
||||||
(remhash id etaf-compiler--artifact-blueprint-owners)
|
|
||||||
(remhash id etaf-compiler--artifact-blueprints))))
|
|
||||||
(remhash name etaf-compiler--active-artifacts)))
|
|
||||||
|
|
||||||
(defun etaf-compiler-note-registry-change ()
|
(defun etaf-compiler-note-registry-change ()
|
||||||
"Invalidate the static Component-call cache after a registry change."
|
"Invalidate the static Component-call cache after a registry change."
|
||||||
@ -62,19 +33,11 @@
|
|||||||
(interactive)
|
(interactive)
|
||||||
(clrhash etaf-compiler--static-cache))
|
(clrhash etaf-compiler--static-cache))
|
||||||
|
|
||||||
(defun etaf-compiler-note-artifact-miss (reason)
|
|
||||||
"Record a rejected or unavailable App artifact REASON."
|
|
||||||
(cl-incf etaf-compiler--artifact-miss-count)
|
|
||||||
(setq etaf-compiler--last-artifact-status reason))
|
|
||||||
|
|
||||||
(defun etaf-compiler-statistics ()
|
(defun etaf-compiler-statistics ()
|
||||||
"Return a read-only snapshot of compiler runtime statistics."
|
"Return a read-only snapshot of compiler runtime statistics."
|
||||||
(list :abi etaf-compiler-blueprint-abi
|
(list :abi etaf-compiler-blueprint-abi
|
||||||
:instantiations etaf-compiler--instantiate-count
|
:instantiations etaf-compiler--instantiate-count
|
||||||
:fallbacks etaf-compiler--fallback-count
|
:fallbacks etaf-compiler--fallback-count
|
||||||
:artifact-hits etaf-compiler--artifact-hit-count
|
|
||||||
:artifact-misses etaf-compiler--artifact-miss-count
|
|
||||||
:last-artifact-status etaf-compiler--last-artifact-status
|
|
||||||
:static-cache-entries (hash-table-count etaf-compiler--static-cache)
|
:static-cache-entries (hash-table-count etaf-compiler--static-cache)
|
||||||
:last-blueprint
|
:last-blueprint
|
||||||
(and etaf-compiler--last-blueprint
|
(and etaf-compiler--last-blueprint
|
||||||
@ -87,146 +50,6 @@
|
|||||||
(plist-get etaf-compiler--last-blueprint :hole-count)))
|
(plist-get etaf-compiler--last-blueprint :hole-count)))
|
||||||
:registry-epoch etaf-compiler--registry-epoch))
|
:registry-epoch etaf-compiler--registry-epoch))
|
||||||
|
|
||||||
(defun etaf-compiler--read-forms (file)
|
|
||||||
"Read every inert Lisp form from FILE."
|
|
||||||
(with-temp-buffer
|
|
||||||
(insert-file-contents file)
|
|
||||||
(goto-char (point-min))
|
|
||||||
(let ((read-eval nil) forms form done)
|
|
||||||
(while (not done)
|
|
||||||
(condition-case nil
|
|
||||||
(progn (setq form (read (current-buffer))) (push form forms))
|
|
||||||
(end-of-file (setq done t))))
|
|
||||||
(nreverse forms))))
|
|
||||||
|
|
||||||
(defun etaf-compiler--read-single-form (file)
|
|
||||||
"Read exactly one inert Lisp form from FILE."
|
|
||||||
(let ((forms (etaf-compiler--read-forms file)))
|
|
||||||
(unless (= (length forms) 1)
|
|
||||||
(signal 'etaf-compiler-artifact-error
|
|
||||||
(list :expected-one-form file (length forms))))
|
|
||||||
(car forms)))
|
|
||||||
|
|
||||||
(defun etaf-compiler--collect-blueprints (form)
|
|
||||||
"Return compiled blueprints found below source FORM."
|
|
||||||
(let (blueprints)
|
|
||||||
(cl-labels
|
|
||||||
((visit (value)
|
|
||||||
(when (consp value)
|
|
||||||
(cond
|
|
||||||
((memq (car value) '(quote function)) nil)
|
|
||||||
((and (memq (car value) '(etaf-view etaf-compiled-view))
|
|
||||||
(cadr value))
|
|
||||||
(when-let* ((compiled (etaf-compiler--compile (cadr value)))
|
|
||||||
(blueprint (car compiled))
|
|
||||||
((etaf-compiler--beneficial-blueprint-p blueprint)))
|
|
||||||
(push blueprint blueprints)))
|
|
||||||
(t (visit (car value))
|
|
||||||
(visit (cdr value)))))))
|
|
||||||
(visit form))
|
|
||||||
(nreverse blueprints)))
|
|
||||||
|
|
||||||
(defun etaf-compiler--source-record (file)
|
|
||||||
"Return the content hash record for FILE."
|
|
||||||
(list :file (file-name-nondirectory file)
|
|
||||||
:sha256 (with-temp-buffer
|
|
||||||
(insert-file-contents-literally file)
|
|
||||||
(secure-hash 'sha256 (current-buffer)))))
|
|
||||||
|
|
||||||
(defun etaf-compiler--write-artifact (artifact output)
|
|
||||||
"Atomically write data-only ARTIFACT to OUTPUT."
|
|
||||||
(let* ((directory (file-name-directory (expand-file-name output)))
|
|
||||||
(_ (make-directory directory t))
|
|
||||||
(temporary (make-temp-file (expand-file-name ".etafc-" directory))))
|
|
||||||
(unwind-protect
|
|
||||||
(progn
|
|
||||||
(with-temp-file temporary
|
|
||||||
(let ((print-circle nil) (print-length nil) (print-level nil))
|
|
||||||
(prin1 artifact (current-buffer))
|
|
||||||
(insert "\n")))
|
|
||||||
(rename-file temporary output t)
|
|
||||||
(setq temporary nil)
|
|
||||||
output)
|
|
||||||
(when (and temporary (file-exists-p temporary))
|
|
||||||
(delete-file temporary)))))
|
|
||||||
|
|
||||||
;;;###autoload
|
|
||||||
(cl-defun etaf-compiler-write-app-artifact
|
|
||||||
(&key name source static style output)
|
|
||||||
"Compile one App into data-only OUTPUT.
|
|
||||||
NAME identifies the App. SOURCE is its Elisp companion, STATIC its inert
|
|
||||||
`.etaf' input, and STYLE its optional `.ecss' input."
|
|
||||||
(unless (and (stringp name) (file-readable-p source)
|
|
||||||
(file-readable-p static) (stringp output))
|
|
||||||
(signal 'wrong-type-argument
|
|
||||||
(list 'etaf-app-source name source static output)))
|
|
||||||
(let (blueprints)
|
|
||||||
(dolist (form (etaf-compiler--read-forms source))
|
|
||||||
(setq blueprints
|
|
||||||
(append blueprints
|
|
||||||
(etaf-compiler--collect-blueprints form))))
|
|
||||||
(setq blueprints
|
|
||||||
(delete-dups blueprints))
|
|
||||||
(let ((artifact
|
|
||||||
(list :kind 'etaf/app-artifact
|
|
||||||
:abi etaf-compiler-app-artifact-abi
|
|
||||||
:app name
|
|
||||||
:sources
|
|
||||||
(delq nil
|
|
||||||
(list (etaf-compiler--source-record source)
|
|
||||||
(etaf-compiler--source-record static)
|
|
||||||
(and style (etaf-compiler--source-record style))))
|
|
||||||
:static-form (etaf-compiler--read-single-form static)
|
|
||||||
:style-form (and style
|
|
||||||
(etaf-compiler--read-single-form style))
|
|
||||||
:blueprints
|
|
||||||
(mapcar (lambda (blueprint)
|
|
||||||
(list :id (plist-get blueprint :id)
|
|
||||||
:blueprint blueprint))
|
|
||||||
blueprints))))
|
|
||||||
(etaf-compiler--write-artifact artifact output))))
|
|
||||||
|
|
||||||
(defun etaf-compiler--validate-source-records (artifact base-directory)
|
|
||||||
"Signal when ARTIFACT does not match BASE-DIRECTORY."
|
|
||||||
(dolist (record (plist-get artifact :sources))
|
|
||||||
(let ((file (expand-file-name (plist-get record :file) base-directory)))
|
|
||||||
(unless (and (file-readable-p file)
|
|
||||||
(equal (with-temp-buffer
|
|
||||||
(insert-file-contents-literally file)
|
|
||||||
(secure-hash 'sha256 (current-buffer)))
|
|
||||||
(plist-get record :sha256)))
|
|
||||||
(signal 'etaf-compiler-stale-artifact
|
|
||||||
(list :source file))))))
|
|
||||||
|
|
||||||
;;;###autoload
|
|
||||||
(defun etaf-load-app-artifact (file base-directory)
|
|
||||||
"Load FILE after validating it against BASE-DIRECTORY."
|
|
||||||
(let ((artifact (etaf-compiler--read-single-form file)))
|
|
||||||
(unless (and (eq (plist-get artifact :kind) 'etaf/app-artifact)
|
|
||||||
(equal (plist-get artifact :abi)
|
|
||||||
etaf-compiler-app-artifact-abi)
|
|
||||||
(stringp (plist-get artifact :app)))
|
|
||||||
(signal 'etaf-compiler-artifact-error (list :manifest file)))
|
|
||||||
(etaf-compiler--validate-source-records artifact base-directory)
|
|
||||||
(etaf-compiler-deactivate-app-artifact (plist-get artifact :app))
|
|
||||||
(dolist (entry (plist-get artifact :blueprints))
|
|
||||||
(let ((blueprint (plist-get entry :blueprint)))
|
|
||||||
(unless (and (equal (plist-get entry :id) (plist-get blueprint :id))
|
|
||||||
(equal (plist-get blueprint :abi)
|
|
||||||
etaf-compiler-blueprint-abi))
|
|
||||||
(signal 'etaf-compiler-artifact-error
|
|
||||||
(list :blueprint entry)))
|
|
||||||
(puthash (plist-get entry :id) blueprint
|
|
||||||
etaf-compiler--artifact-blueprints)
|
|
||||||
(cl-pushnew (plist-get artifact :app)
|
|
||||||
(gethash (plist-get entry :id)
|
|
||||||
etaf-compiler--artifact-blueprint-owners)
|
|
||||||
:test #'equal)))
|
|
||||||
(puthash (plist-get artifact :app) artifact
|
|
||||||
etaf-compiler--active-artifacts)
|
|
||||||
(setq etaf-compiler--last-artifact-status 'loaded)
|
|
||||||
artifact))
|
|
||||||
|
|
||||||
(defun etaf-compiler--literal (form)
|
(defun etaf-compiler--literal (form)
|
||||||
"Return `(t . VALUE)' when FORM is a portable literal, otherwise nil."
|
"Return `(t . VALUE)' when FORM is a portable literal, otherwise nil."
|
||||||
(cond
|
(cond
|
||||||
@ -381,7 +204,7 @@ NAME identifies the App. SOURCE is its Elisp companion, STATIC its inert
|
|||||||
|
|
||||||
;;;###autoload
|
;;;###autoload
|
||||||
(defun etaf-compiler-instantiate (blueprint programs)
|
(defun etaf-compiler-instantiate (blueprint programs)
|
||||||
"Instantiate compiled View BLUEPRINT with dynamic PROGRAMS."
|
"Instantiate automatically lowered View BLUEPRINT with dynamic PROGRAMS."
|
||||||
(unless (and (eq (plist-get blueprint :kind) 'etaf/view-blueprint)
|
(unless (and (eq (plist-get blueprint :kind) 'etaf/view-blueprint)
|
||||||
(equal (plist-get blueprint :abi)
|
(equal (plist-get blueprint :abi)
|
||||||
etaf-compiler-blueprint-abi)
|
etaf-compiler-blueprint-abi)
|
||||||
@ -389,49 +212,21 @@ NAME identifies the App. SOURCE is its Elisp companion, STATIC its inert
|
|||||||
(vectorp programs)
|
(vectorp programs)
|
||||||
(= (length programs) (plist-get blueprint :hole-count)))
|
(= (length programs) (plist-get blueprint :hole-count)))
|
||||||
(error "Invalid or incompatible ETAF View blueprint"))
|
(error "Invalid or incompatible ETAF View blueprint"))
|
||||||
(let ((artifact-blueprint
|
(cl-incf etaf-compiler--instantiate-count)
|
||||||
(gethash (plist-get blueprint :id)
|
(setq etaf-compiler--last-blueprint blueprint)
|
||||||
etaf-compiler--artifact-blueprints)))
|
(etaf-compiler--materialize
|
||||||
(if artifact-blueprint
|
blueprint (plist-get blueprint :root) programs))
|
||||||
(progn
|
|
||||||
(cl-incf etaf-compiler--artifact-hit-count)
|
|
||||||
(setq etaf-compiler--last-artifact-status 'hit
|
|
||||||
blueprint artifact-blueprint))
|
|
||||||
(cl-incf etaf-compiler--artifact-miss-count)
|
|
||||||
(setq etaf-compiler--last-artifact-status 'embedded-fallback))
|
|
||||||
(cl-incf etaf-compiler--instantiate-count)
|
|
||||||
(setq etaf-compiler--last-blueprint blueprint)
|
|
||||||
(etaf-compiler--materialize
|
|
||||||
blueprint (plist-get blueprint :root) programs)))
|
|
||||||
|
|
||||||
(defun etaf-compiler-expand-view
|
(defun etaf-compiler-expand-view (form &optional slot-mode)
|
||||||
(form &optional slot-mode artifact-required count-fallback)
|
|
||||||
"Return compiler expansion for View FORM.
|
"Return compiler expansion for View FORM.
|
||||||
SLOT-MODE is forwarded to the legacy compiler. ARTIFACT-REQUIRED keeps the
|
SLOT-MODE is forwarded to the legacy compiler."
|
||||||
legacy path unless a manually loaded artifact contains the blueprint.
|
|
||||||
COUNT-FALLBACK records an unsupported explicit compiler request."
|
|
||||||
(let* ((compiled (etaf-compiler--compile form))
|
(let* ((compiled (etaf-compiler--compile form))
|
||||||
(blueprint (car compiled))
|
(blueprint (car compiled))
|
||||||
(legacy (etaf--compile-view-form form (or slot-mode :projection))))
|
(legacy (etaf--compile-view-form form (or slot-mode :projection))))
|
||||||
(if (and compiled (etaf-compiler--beneficial-blueprint-p blueprint))
|
(if (and compiled (etaf-compiler--beneficial-blueprint-p blueprint))
|
||||||
(let ((instantiate
|
`(etaf-compiler-instantiate
|
||||||
`(etaf-compiler-instantiate
|
',blueprint (vector ,@(cdr compiled)))
|
||||||
',blueprint (vector ,@(cdr compiled)))))
|
`(progn (cl-incf etaf-compiler--fallback-count) ,legacy))))
|
||||||
(if artifact-required
|
|
||||||
`(if (gethash ,(plist-get blueprint :id)
|
|
||||||
etaf-compiler--artifact-blueprints)
|
|
||||||
,instantiate
|
|
||||||
,legacy)
|
|
||||||
instantiate))
|
|
||||||
(if count-fallback
|
|
||||||
`(progn (cl-incf etaf-compiler--fallback-count) ,legacy)
|
|
||||||
legacy))))
|
|
||||||
|
|
||||||
;;;###autoload
|
|
||||||
(defmacro etaf-compiled-view (form)
|
|
||||||
"Compatibility spelling for explicitly compiled View FORM."
|
|
||||||
(declare (indent 1) (debug (form)))
|
|
||||||
(etaf-compiler-expand-view form :projection nil t))
|
|
||||||
|
|
||||||
(provide 'etaf-compiler)
|
(provide 'etaf-compiler)
|
||||||
;;; etaf-compiler.el ends here
|
;;; etaf-compiler.el ends here
|
||||||
|
|||||||
@ -66,7 +66,6 @@ ELAPSED is milliseconds; START-TIME and END-TIME are wall-clock seconds."
|
|||||||
(etaf-data-source-load-page data-prepare)
|
(etaf-data-source-load-page data-prepare)
|
||||||
(etaf-data-mutate data-mutate)
|
(etaf-data-mutate data-mutate)
|
||||||
(etaf-resource-load resource-load)
|
(etaf-resource-load resource-load)
|
||||||
(etaf-compile-app app-compile)
|
|
||||||
(ebox-surface-update-buffer-viewport viewport)
|
(ebox-surface-update-buffer-viewport viewport)
|
||||||
(ebox-rerender-buffer-with-context rerender)
|
(ebox-rerender-buffer-with-context rerender)
|
||||||
(etaf-focus focus)
|
(etaf-focus focus)
|
||||||
@ -86,8 +85,6 @@ ELAPSED is milliseconds; START-TIME and END-TIME are wall-clock seconds."
|
|||||||
(etaf--runtime-render-dirty-inline-range runtime inline-range-effect)
|
(etaf--runtime-render-dirty-inline-range runtime inline-range-effect)
|
||||||
(etaf--runtime-render-dirty-slot-range runtime slot-range-effect)
|
(etaf--runtime-render-dirty-slot-range runtime slot-range-effect)
|
||||||
(etaf-compiler-instantiate compiler blueprint-instantiate)
|
(etaf-compiler-instantiate compiler blueprint-instantiate)
|
||||||
(etaf-compiler-write-app-artifact compiler app-artifact-write)
|
|
||||||
(etaf-load-app-artifact compiler app-artifact-load)
|
|
||||||
(etaf-sqlite--call sqlite call)
|
(etaf-sqlite--call sqlite call)
|
||||||
(etaf-sqlite--transaction sqlite transaction)
|
(etaf-sqlite--transaction sqlite transaction)
|
||||||
(etaf-sqlite--select-items sqlite query)
|
(etaf-sqlite--select-items sqlite query)
|
||||||
@ -428,7 +425,6 @@ When called interactively, suggest a timestamped `.eld' file in
|
|||||||
(if (etaf-performance--runtime-p first)
|
(if (etaf-performance--runtime-p first)
|
||||||
first
|
first
|
||||||
(and (boundp 'etaf--current-runtime) etaf--current-runtime)))
|
(and (boundp 'etaf--current-runtime) etaf--current-runtime)))
|
||||||
((eq function 'etaf-compile-app) nil)
|
|
||||||
((memq function '(ebox-surface-update-buffer-viewport
|
((memq function '(ebox-surface-update-buffer-viewport
|
||||||
ebox-rerender-buffer-with-context))
|
ebox-rerender-buffer-with-context))
|
||||||
(etaf-performance--runtime-for-buffer first))
|
(etaf-performance--runtime-for-buffer first))
|
||||||
@ -484,7 +480,6 @@ FUNCTION received ARGUMENTS and produced RESULT when AFTER is non-nil."
|
|||||||
(or (nth 2 arguments) 1) (or (nth 3 arguments) 20)))
|
(or (nth 2 arguments) 1) (or (nth 3 arguments) 20)))
|
||||||
('etaf-data-mutate (format "data %S" (nth 1 arguments)))
|
('etaf-data-mutate (format "data %S" (nth 1 arguments)))
|
||||||
('etaf-resource-load "resource load/reload")
|
('etaf-resource-load "resource load/reload")
|
||||||
('etaf-compile-app (format "%s" (or (car arguments) "default App")))
|
|
||||||
('ebox-surface-update-buffer-viewport
|
('ebox-surface-update-buffer-viewport
|
||||||
(format "viewport %sx%s" (nth 1 arguments) (or (nth 2 arguments) "-")))
|
(format "viewport %sx%s" (nth 1 arguments) (or (nth 2 arguments) "-")))
|
||||||
('ebox-rerender-buffer-with-context
|
('ebox-rerender-buffer-with-context
|
||||||
|
|||||||
@ -442,7 +442,7 @@ ordinary Elisp expressions. `expr' is the only computation bridge in the
|
|||||||
child region and accepts only `:value'."
|
child region and accepts only `:value'."
|
||||||
(declare (indent 1) (debug (form)))
|
(declare (indent 1) (debug (form)))
|
||||||
(if (fboundp 'etaf-compiler-expand-view)
|
(if (fboundp 'etaf-compiler-expand-view)
|
||||||
(etaf-compiler-expand-view form :projection t)
|
(etaf-compiler-expand-view form :projection)
|
||||||
(etaf--compile-view-form form :projection)))
|
(etaf--compile-view-form form :projection)))
|
||||||
|
|
||||||
(defun etaf--component-prop-key (name)
|
(defun etaf--component-prop-key (name)
|
||||||
|
|||||||
1
etaf.el
1
etaf.el
@ -29,7 +29,6 @@
|
|||||||
|
|
||||||
(require 'etaf-view)
|
(require 'etaf-view)
|
||||||
(require 'etaf-compiler)
|
(require 'etaf-compiler)
|
||||||
(require 'etaf-app)
|
|
||||||
(require 'etaf-component)
|
(require 'etaf-component)
|
||||||
(require 'etaf-reactive)
|
(require 'etaf-reactive)
|
||||||
(require 'etaf-context)
|
(require 'etaf-context)
|
||||||
|
|||||||
@ -1,91 +0,0 @@
|
|||||||
;;; etaf-app-tests.el --- ETAF App workflow tests -*- lexical-binding: t; -*-
|
|
||||||
|
|
||||||
;;; Code:
|
|
||||||
|
|
||||||
(require 'ert)
|
|
||||||
(require 'etaf)
|
|
||||||
|
|
||||||
(defmacro etaf-app-test--with-files (bindings &rest body)
|
|
||||||
"Create temporary App files named by BINDINGS and evaluate BODY."
|
|
||||||
(declare (indent 1))
|
|
||||||
`(let* ((directory (make-temp-file "etaf-app-test-" t))
|
|
||||||
(source (expand-file-name "sample.el" directory))
|
|
||||||
(static (expand-file-name "sample.etaf" directory))
|
|
||||||
(style (expand-file-name "sample.ecss" directory))
|
|
||||||
(artifact (expand-file-name "sample.etafc" directory))
|
|
||||||
,@bindings)
|
|
||||||
(unwind-protect
|
|
||||||
(progn
|
|
||||||
(with-temp-file source
|
|
||||||
(insert "(defun sample-view (color)\n"
|
|
||||||
" (etaf-view\n"
|
|
||||||
" (column :color color (text \"sample\"))))\n"))
|
|
||||||
(with-temp-file static (insert "(sample :title \"Sample\")\n"))
|
|
||||||
(with-temp-file style (insert "(styles (\"column\" :color red))\n"))
|
|
||||||
,@body)
|
|
||||||
(when (file-directory-p directory)
|
|
||||||
(delete-directory directory t)))))
|
|
||||||
|
|
||||||
(ert-deftest etaf-app-interactive-workflow-produces-current-artifact ()
|
|
||||||
"The public App command writes and loads a current standalone artifact."
|
|
||||||
(etaf-app-test--with-files ()
|
|
||||||
(etaf-register-app "sample" :source source :static static :style style
|
|
||||||
:artifact artifact)
|
|
||||||
(should (eq (plist-get (etaf-app-compile-status "sample") :status)
|
|
||||||
'missing))
|
|
||||||
(let ((status (etaf-compile-app "sample")))
|
|
||||||
(should (file-readable-p artifact))
|
|
||||||
(should (eq (plist-get status :status) 'current))
|
|
||||||
(should (= (plist-get status :blueprints) 1)))
|
|
||||||
(should (eq (plist-get (etaf-app-compile-status "sample") :status)
|
|
||||||
'current))))
|
|
||||||
|
|
||||||
(ert-deftest etaf-app-artifact-becomes-stale-after-source-change ()
|
|
||||||
"Changing any source hash rejects the previously compiled artifact."
|
|
||||||
(etaf-app-test--with-files ()
|
|
||||||
(etaf-register-app "sample-stale"
|
|
||||||
:source source :static static :style style
|
|
||||||
:artifact artifact)
|
|
||||||
(etaf-compile-app "sample-stale")
|
|
||||||
(with-temp-buffer
|
|
||||||
(insert-file-contents source)
|
|
||||||
(goto-char (point-max))
|
|
||||||
(insert "\n;; changed\n")
|
|
||||||
(write-region (point-min) (point-max) source nil 'silent))
|
|
||||||
(should (eq (plist-get (etaf-app-compile-status "sample-stale") :status)
|
|
||||||
'stale))))
|
|
||||||
|
|
||||||
(ert-deftest etaf-app-loaded-blueprint-is-consumed-at-runtime ()
|
|
||||||
"Instantiation uses the manually compiled blueprint by artifact id."
|
|
||||||
(etaf-app-test--with-files ()
|
|
||||||
(etaf-register-app "sample-hit" :source source :static static :style style
|
|
||||||
:artifact artifact)
|
|
||||||
(let* ((status (etaf-compile-app "sample-hit"))
|
|
||||||
(entry (car (plist-get (plist-get status :artifact) :blueprints)))
|
|
||||||
(blueprint (plist-get entry :blueprint))
|
|
||||||
(before (plist-get (etaf-compiler-statistics) :artifact-hits))
|
|
||||||
(view (etaf-compiler-instantiate
|
|
||||||
blueprint (vector (lambda () "purple")))))
|
|
||||||
(should (etaf--view-node-p view))
|
|
||||||
(should (> (plist-get (etaf-compiler-statistics) :artifact-hits) before)))))
|
|
||||||
|
|
||||||
(ert-deftest etaf-app-missing-artifact-warns-once-with-compile-command ()
|
|
||||||
"Direct fallback tells an interactive user exactly how to compile the App."
|
|
||||||
(etaf-app-test--with-files ()
|
|
||||||
(etaf-register-app "sample-warning"
|
|
||||||
:source source :static static :style style
|
|
||||||
:artifact artifact)
|
|
||||||
(let ((noninteractive nil)
|
|
||||||
(etaf-app-warn-on-artifact-fallback t)
|
|
||||||
warnings)
|
|
||||||
(remhash "sample-warning" etaf-app--warning-state)
|
|
||||||
(cl-letf (((symbol-function 'display-warning)
|
|
||||||
(lambda (_type message &optional _level _buffer-name)
|
|
||||||
(push message warnings))))
|
|
||||||
(should-not (etaf-app-load-artifact-or-warn "sample-warning"))
|
|
||||||
(should-not (etaf-app-load-artifact-or-warn "sample-warning")))
|
|
||||||
(should (= (length warnings) 1))
|
|
||||||
(should (string-match-p "M-x etaf-compile-app" (car warnings))))))
|
|
||||||
|
|
||||||
(provide 'etaf-app-tests)
|
|
||||||
;;; etaf-app-tests.el ends here
|
|
||||||
@ -35,17 +35,17 @@
|
|||||||
(text "static"))
|
(text "static"))
|
||||||
(text (expr :value text)))))
|
(text (expr :value text)))))
|
||||||
|
|
||||||
(defun etaf-compiler-test--compiled (color text)
|
(defun etaf-compiler-test--lowered (color text)
|
||||||
"Return a compiled fixture using COLOR and TEXT."
|
"Return an automatically lowered fixture using COLOR and TEXT."
|
||||||
(etaf-compiled-view
|
(etaf-view
|
||||||
(column :color color
|
(column :color color
|
||||||
(column :padding '(1 2)
|
(column :padding '(1 2)
|
||||||
(text "static"))
|
(text "static"))
|
||||||
(text (expr :value text)))))
|
(text (expr :value text)))))
|
||||||
|
|
||||||
(defun etaf-compiler-test--fallback-view ()
|
(defun etaf-compiler-test--fallback-view ()
|
||||||
"Return a View containing the not-yet-compiled slot grammar."
|
"Return a View containing unsupported slot grammar."
|
||||||
(etaf-compiled-view
|
(etaf-view
|
||||||
(column (slot (text "fallback")))))
|
(column (slot (text "fallback")))))
|
||||||
|
|
||||||
(defun etaf-compiler-test--fallback-reference ()
|
(defun etaf-compiler-test--fallback-reference ()
|
||||||
@ -53,7 +53,7 @@
|
|||||||
(etaf-compiler-test--legacy-view
|
(etaf-compiler-test--legacy-view
|
||||||
(column (slot (text "fallback")))))
|
(column (slot (text "fallback")))))
|
||||||
|
|
||||||
(ert-deftest etaf-compiled-view-fallback-is-exact ()
|
(ert-deftest etaf-automatic-view-fallback-is-exact ()
|
||||||
"An unsupported slot keeps the existing View semantics."
|
"An unsupported slot keeps the existing View semantics."
|
||||||
(let ((before (plist-get (etaf-compiler-statistics) :fallbacks)))
|
(let ((before (plist-get (etaf-compiler-statistics) :fallbacks)))
|
||||||
(should
|
(should
|
||||||
@ -64,9 +64,9 @@
|
|||||||
(should (= (1+ before)
|
(should (= (1+ before)
|
||||||
(plist-get (etaf-compiler-statistics) :fallbacks)))))
|
(plist-get (etaf-compiler-statistics) :fallbacks)))))
|
||||||
|
|
||||||
(defun etaf-compiler-test--compiled-supported (color)
|
(defun etaf-compiler-test--lowered-supported (color)
|
||||||
"Return a supported compiled fixture using COLOR."
|
"Return a supported automatically lowered fixture using COLOR."
|
||||||
(etaf-compiled-view
|
(etaf-view
|
||||||
(column :color color
|
(column :color color
|
||||||
(column :padding '(1 2)
|
(column :padding '(1 2)
|
||||||
(text "static-a")
|
(text "static-a")
|
||||||
@ -82,35 +82,35 @@
|
|||||||
(text "static-b"))
|
(text "static-b"))
|
||||||
(text :color "blue" "tail"))))
|
(text :color "blue" "tail"))))
|
||||||
|
|
||||||
(ert-deftest etaf-compiled-view-supported-output-is-exact ()
|
(ert-deftest etaf-automatic-view-supported-output-is-exact ()
|
||||||
"A supported blueprint produces the same normalized View data."
|
"A supported blueprint produces the same normalized View data."
|
||||||
(should
|
(should
|
||||||
(equal (etaf-compiler-test--canonical
|
(equal (etaf-compiler-test--canonical
|
||||||
(etaf-compiler-test--compiled-supported "green"))
|
(etaf-compiler-test--lowered-supported "green"))
|
||||||
(etaf-compiler-test--canonical
|
(etaf-compiler-test--canonical
|
||||||
(etaf-compiler-test--interpreted-supported "green")))))
|
(etaf-compiler-test--interpreted-supported "green")))))
|
||||||
|
|
||||||
(ert-deftest etaf-compiled-view-reuses-static-subtrees ()
|
(ert-deftest etaf-automatic-view-reuses-static-subtrees ()
|
||||||
"Repeated instantiation reuses a static child while rebuilding its root."
|
"Repeated instantiation reuses a static child while rebuilding its root."
|
||||||
(etaf-compiler-clear-cache)
|
(etaf-compiler-clear-cache)
|
||||||
(let* ((first (etaf-compiler-test--compiled-supported "red"))
|
(let* ((first (etaf-compiler-test--lowered-supported "red"))
|
||||||
(second (etaf-compiler-test--compiled-supported "blue"))
|
(second (etaf-compiler-test--lowered-supported "blue"))
|
||||||
(first-static (car (etaf--view-node-children first)))
|
(first-static (car (etaf--view-node-children first)))
|
||||||
(second-static (car (etaf--view-node-children second))))
|
(second-static (car (etaf--view-node-children second))))
|
||||||
(should-not (eq first second))
|
(should-not (eq first second))
|
||||||
(should (eq first-static second-static))))
|
(should (eq first-static second-static))))
|
||||||
|
|
||||||
(ert-deftest etaf-compiled-view-reduces-runtime-construction-work ()
|
(ert-deftest etaf-automatic-view-reduces-runtime-construction-work ()
|
||||||
"A warmed blueprint constructs only its dynamic ancestor path."
|
"A warmed blueprint constructs only its dynamic ancestor path."
|
||||||
(etaf-compiler-clear-cache)
|
(etaf-compiler-clear-cache)
|
||||||
(etaf-compiler-test--compiled-supported "warm")
|
(etaf-compiler-test--lowered-supported "warm")
|
||||||
(let ((calls 0)
|
(let ((calls 0)
|
||||||
(original (symbol-function 'etaf--view-call)))
|
(original (symbol-function 'etaf--view-call)))
|
||||||
(cl-letf (((symbol-function 'etaf--view-call)
|
(cl-letf (((symbol-function 'etaf--view-call)
|
||||||
(lambda (&rest arguments)
|
(lambda (&rest arguments)
|
||||||
(cl-incf calls)
|
(cl-incf calls)
|
||||||
(apply original arguments))))
|
(apply original arguments))))
|
||||||
(etaf-compiler-test--compiled-supported "next"))
|
(etaf-compiler-test--lowered-supported "next"))
|
||||||
(should (= calls 1))
|
(should (= calls 1))
|
||||||
(setq calls 0)
|
(setq calls 0)
|
||||||
(cl-letf (((symbol-function 'etaf--view-call)
|
(cl-letf (((symbol-function 'etaf--view-call)
|
||||||
@ -120,7 +120,7 @@
|
|||||||
(etaf-compiler-test--interpreted-supported "next"))
|
(etaf-compiler-test--interpreted-supported "next"))
|
||||||
(should (> calls 1))))
|
(should (> calls 1))))
|
||||||
|
|
||||||
(ert-deftest etaf-compiled-view-exposes-blueprint-coverage ()
|
(ert-deftest etaf-automatic-view-exposes-blueprint-coverage ()
|
||||||
"The compiler reports static nodes, dynamic paths, and holes."
|
"The compiler reports static nodes, dynamic paths, and holes."
|
||||||
(pcase-let* ((`(,blueprint . ,programs)
|
(pcase-let* ((`(,blueprint . ,programs)
|
||||||
(etaf-compiler--compile
|
(etaf-compiler--compile
|
||||||
@ -135,7 +135,7 @@
|
|||||||
(should (= (length programs) 1))
|
(should (= (length programs) 1))
|
||||||
(should (= (plist-get blueprint :hole-count) 1))))
|
(should (= (plist-get blueprint :hole-count) 1))))
|
||||||
|
|
||||||
(ert-deftest etaf-compiled-view-expr-is-a-dynamic-child-hole ()
|
(ert-deftest etaf-automatic-view-expr-is-a-dynamic-child-hole ()
|
||||||
"Expr becomes one dynamic child program without forcing root fallback."
|
"Expr becomes one dynamic child program without forcing root fallback."
|
||||||
(pcase-let* ((`(,blueprint . ,programs)
|
(pcase-let* ((`(,blueprint . ,programs)
|
||||||
(etaf-compiler--compile
|
(etaf-compiler--compile
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user