35 KiB
ETAF Component 定义:历史目标设计
SUPERSEDED / historical:本文不再是当前公共合同。
当前公共合同以 docs/architecture.zh.md 与 docs/user-guide.zh.md 为准。本文保留为设计决策历史,不再以 创建时的“当前状态”表覆盖已经交付的实现。
本文只拥有 Component 作者语法和作者可见语义。属性 owner、跨包依赖、 transaction 权限、Ebox/TP/Rust 边界仍以 module-boundaries.zh.md 为准;本文只引用,不复制第二套 系统规范。本文的 DSL 选择取代旧 capability-expansion 草案中的 Structural Expr 方向;对应计划和测试规格必须原子更新后才能实施。
1. 目标
ETAF 只保留一种 Component 语义模型,但允许用户用两种严格分开的方式定义它:
- DSL 模式:类似 Vue template,结构由 tag、attribute、slot 和少量编译指令表达;
- 代码模式:类似 React render function,结构由普通 Elisp 和唯一节点构造函数
etaf-node表达。
两种写法只是两个 authoring frontend。它们产生同一种规范 View IR,共用同一个:
- Component Registry;
- props 与 slots 契约;
- retained Component identity;
- reactive Scope;
- Context 与 Theme;
- lifecycle;
- Range/Fragment;
- Runtime generation;
- Ebox lowering;
- TP/Emacs 原子提交与回滚。
不能因为支持两种写法而产生两套 Component、两套节点、两条 Runtime 或两种生命周期。
2. 最小心智模型
用户只需先理解五个概念:
- Component:可复用、可保留状态和生命周期的语义单元;
- Host:真正参与文本、盒模型和布局的视觉节点;
- props:父级传入的只读输入;
- state:Component 实例自己保留的状态;
- children/slots:父级交给 Component 的子内容。
然后二选一:
喜欢声明式模板 → :view DSL
喜欢完整 Elisp → :render + etaf-node
普通用户不需要理解 Ebox node id、TP object、buffer text property、compiler block、 Runtime generation 或 Rust tape。
3. 唯一 Component 定义外壳
两种模式都使用同一个定义入口:
(etaf-define-component NAME (&key PROP ...)
[DOCSTRING]
[:setup SETUP-FORM]
(:view VIEW-FORM | :render RENDER-FORM)
[:styles STYLES-FORM])
规则:
NAME是 Component Registry 中的准确 symbol;- props 只使用
(&key PROP ...)声明; :view与:render必须且只能出现一个;:setup只初始化实例;返回值通过显式函数etaf-state读取,不能返回 render function;:styles是可选静态 Component stylesheet;- 未知、重复或不完整 clause 在定义时直接报错;
- 不存在根据返回值猜测定义模式的行为。
推荐固定按下面的顺序书写:
docstring → :setup → :view/:render → :styles
解析器可以按 keyword 识别 clause,但正式示例只展示这一种顺序。
4. Component 名称与命名空间
Component tag 属于 ETAF Component Registry,不属于 Elisp function namespace。
(etaf-define-component book-detail (&key book)
:view
(box (expr (book-title book))))
(defun book-detail (book)
;; 普通 Elisp function;不会改变上面的 Component tag。
...)
在 DSL 的 View 位置中,(book-detail ...) 永远查 Component Registry;在普通 Elisp
函数位置中,(book-detail value) 永远调用 Elisp function。ETAF 不使用 fboundp、
load order 或返回值猜测二者。
Registry 中的重复 Component name 是错误。框架/库作者应使用明确前缀或 namespaced
symbol;受控应用可以使用短名。核心不自动删除 etaf- 前缀,也不根据当前已加载
function 生成 alias,因为这种行为会让同一源码随加载顺序改变语义。
5. DSL 模式::view
5.1 基本定义
(etaf-define-component status-card (&key title message)
"Render one status card."
:view
(column :class '(status-card)
(text :class '(status-card__title) (expr title))
(box :class '(status-card__body)
(expr message))))
Component 的 :view 本身就是 DSL 编译边界,里面不再套 etaf-view。
5.2 DSL 的唯一结构
(TAG :PROPERTY VALUE ... CHILD ...)
规则:
- 所有属性必须位于所有 child 之前;
TAG必须是 Registry 中的 Host 或 Component;- Host/Component 位置不执行同名 Elisp function;
- property value 是普通 Elisp value expression;
- child 位置只接受字符串、nil、DSL node、slot 或
(expr FORM); - DSL node 不 quote;被 quote 的 list 只是数据,不会变成 View;
- raw list、vector 或任意 Elisp control form 不能直接充当 child。
5.3 属性值仍是普通 Elisp
(box :width '(viewport)
:color (if active "#ffffff" "#777777")
:on-press (lambda () (select-item item-id))
:key (book-id book)
...)
这里:
'(viewport)遵循普通 Elisp quote;(if ...)是属性值表达式;(select-item ...)是 callback 中的 Elisp function;(book-id book)是计算 key 的 Elisp function;- 只有最外层
(box ...)处于 DSL View 位置。
5.4 Core Host
目标 core Host 只有:
| Host | 作用 |
|---|---|
text |
显式 Text;需要文本属性时使用。 |
box |
普通 Box;默认 Normal layout。 |
row |
Box + Row layout 的作者糖。 |
column |
Box + Column layout 的作者糖。 |
flex |
Box + Flex layout 的作者糖。 |
grid |
Box + Grid layout 的作者糖。 |
fragment |
无视觉几何的透明 child group/Range。 |
普通字符串 child 自动规范化为 Text:
(box "Hello")
等价于没有额外 Text 属性的:
(box (text "Hello"))
需要 color、font、ref 或其他 Text 能力时显式写 text。fragment 不参与布局,
不伪装成 Box,也不引入公开 flow、spacer、item 或 grid-item 节点。
视觉属性的完整 schema、适用 Host、alias、shorthand 和 ParentParticipation 由 module-boundaries.zh.md 的统一 property schema 单独定义; 本文不复制第二份属性表。
5.5 文本插值:expr
DSL 中唯一的动态文本桥接是:
(expr FORM)
例如:
(text (expr (concat "Selected: "
(plist-get selected-book :title))))
expr 的目标契约:
- 没有
:value; - 没有 attrs 或 children;
FORM是普通 Elisp;- 结果只能是 nil、string 或 propertized string;
- nil 表示空文本;
- 不能返回 View、Component、Fragment 或 node sequence;
- 不能承担 selection、loop、key 或 lifecycle;
- property value 已经是 Elisp value position,不使用
expr。
因此 expr 只对应模板文本插值,不再是结构逃生口。
5.6 结构指令
DSL 第一版只保留四个 compiler-owned directive:
:if
:else-if
:else
:for
以及已有 identity metadata:
:key
条件
(column
(book-detail :if selected-book :book selected-book)
(empty-detail :else t))
(column
(loading-view :if loading-p)
(error-view :else-if error :error error)
(book-list :else t :books books))
规则:
:else的值必须是 literalt;:else-if/:else必须直接相邻地跟在同级:if后;- chain 不能跨 Fragment、Component、slot 或 opaque boundary;
- 缺少最终
:else等价于空 arm; - branch 切换只有在新 generation 成功后才释放旧 arm;
- 失败时恢复原 arm、identity、文本与属性。
循环
(column
(book-row :for (book books)
:key (book-id book)
:book book))
规则:
- 语法固定为
:for (ITEM ITEMS); ITEMS每次 update 只求值一次;ITEM只在该 node 的 key、props、children 和 interpolation 中可见;:key必填;- key 只能是非 nil symbol、integer 或防御性复制的 string;
- key 用
equal比较; - float、隐式 index key、重复 key 在 candidate mutation 前报错;
- 任一 branch directive(
:if、:else-if、:else)都不能与:for出现在同一 node;所有组合都是 compile error,没有隐藏优先级; - 多 node item 使用
fragment,不增加视觉 Box。
:if、:else-if、:else、:for 在 compiler lowering 后被移除,Component props、
ECSS、Ebox、TP 和 Emacs adapter 都看不到 directive。
第一版不加入 custom directive registry、:show、:await、:model、:memo、
transition 或其他 convenience。它们只有经过独立真实场景证明后才能新增。
6. 代码模式::render
6.1 基本定义
代码模式的 render body 是普通 Elisp:
(etaf-define-component book-list (&key books selected-book)
:render
(let ((children
(append
(when selected-book
(list
(etaf-node
'book-detail
(list :book selected-book)
nil)))
(mapcar
(lambda (book)
(etaf-node
'book-row
(list :key (book-id book)
:book book)
nil))
books))))
(etaf-node 'column nil children)))
这里的 let、when、append、mapcar、lambda 和 book-id 全部是普通 Elisp。
ETAF compiler 不把它们解释成 DSL。
6.2 唯一节点构造函数
代码模式只用一个公共节点构造函数:
(etaf-node TAG PROPS CHILDREN &optional NAMED-SLOTS)
参数契约:
TAG:准确的 Host/Component Registry symbol;PROPS:已经求值的 keyword/value plist,或 nil;CHILDREN:已经求值的 child list,或 nil;NAMED-SLOTS:只供 Component call 使用的(NAME . CHILD-LIST)alist;- 返回经过验证、带当前 View IR ABI 的 typed View value。
示例:
(etaf-node 'box
(list :padding 1 :background-color "#20242a")
(list "Hello"))
(etaf-node 'panel
(list :key panel-id)
default-children
(list (cons 'header header-children)
(cons 'actions action-children)))
etaf-node 的约束:
- 它是普通 function,不是 macro;
TAG通常要 quote,因为它是 Elisp symbol value;- 它不接收 DSL source list;
PROPS中不接受:if、:else-if、:else、:for;- 条件与循环直接使用 Elisp;
- 动态文本直接使用 string,不使用
expr; - string child 自动规范化为 Text;
- nil child 被忽略;
- raw source list、任意 object 或未验证 sequence 被拒绝;
- named slots 只能用于 Component,Host 收到它时报错;
- Component 的
:key是 identity metadata,不传入业务 props。
多根结果显式使用 Fragment:
(etaf-node 'fragment nil nodes)
不允许 render body 直接返回一个 node list,因为“一个 View”与“多个 siblings”必须 有不同类型,不能再次依靠 list 形状猜测。
6.3 Render 结果
:render 只能返回:
nil | string/propertized-string | one typed View value
需要多个 sibling 时返回 Fragment。不能返回:
- DSL source form;
- quoted/quasiquoted View list;
- node vector/list;
- Ebox node;
- buffer string patch;
- render closure。
严格分离发生在 Component 的定义边界::render body 不写 DSL source form,
:view body 不写普通结构 control。已经由独立 helper 返回的、通过 ABI validator 的
typed View 可以被代码模式组合;Runtime 不保存 frontend provenance,也不反向猜测
这个 typed value 最初由 macro 还是 function 构造。给共享 IR 增加 provenance 只为
禁止 helper 组合会制造第二种无业务价值的身份,因此不采用。
6.4 纯 render 纪律
代码模式允许完整 Elisp control flow,但 render 必须是框架意义上的纯计算:
- 可以读取 props、
(etaf-state)返回对象中的 ref/computed、Context、Theme 和 Resource/Data state; - 可以调用无副作用 helper;
- 可以创建短生命周期 typed View value;
- 不能写 ref、发 Action、修改 Data、注册 Effect/lifecycle、mount/unmount 或写 buffer;
- 不能在 render 中创建需要 cleanup 的长期资源;
- Runtime 应在 render phase 拒绝可检测的 mutation,而不是让副作用重复执行。
需要副作用、订阅或 cleanup 的代码放进 :setup、event callback、Action、Behavior、
watch/effect 或 Resource/Data owner。
6.5 Unkeyed 与 keyed identity
代码模式没有 DSL compiler 的静态 call-site token。未加 :key 的 sibling 按父级、
ordinal 与 tag 参与 identity;固定结构可以使用它。任何可能插入、删除或重排的
collection 都必须使用稳定 :key,否则状态按位置迁移属于作者错误。
ETAF 不根据 mapcar、cl-loop 或返回 list 猜测“这是动态列表”,也不偷偷生成
index key。
7. Props
(etaf-define-component user-card (&key user compact on-open)
...)
规则:
- 声明只接受普通 symbol;
- 每个 prop 对应同名 keyword,例如
user对应:user; - 未传 prop 的值为 nil;
- 未声明为业务 prop、也不属于统一 Host schema 的名称在调用边界报错;
- 重复 prop、重复 canonical Host property domain、奇数 plist 在调用边界报错;
- props 在一次 render 中是只读 snapshot;
- prop 更新不重新运行 setup,只触发依赖它的 render/patch;
:key和 directive 不会进入 Component 业务 props;- Component 显式声明的同名属性优先归入业务 props;其他合法 Ebox style/layout 属性 与 ETAF Host metadata 归入独立 attrs,并只透传到单一根 Host;
- 第一版不增加另一套 required/default/type schema。需要校验时在 setup 或普通 helper 中显式完成,后续只有真实重复证据才能扩展 prop declaration。
在 :view 和 :render 中,声明的 prop symbol 绑定到当前 Component input,不依赖
同名全局变量。局部 :for item 不能与 prop 重名。
7.1 输入 namespace
同一 keyword plist 在解析时按 node kind 使用一个确定的 namespace:
| 输入 | Host | Component | Fragment | etaf-node 代码模式 |
|---|---|---|---|---|
:if/:else-if/:else/:for |
DSL directive | DSL directive | DSL directive | 全部拒绝 |
:key |
framework identity | framework identity | framework identity | 同一 identity 规则 |
| Host schema property/metadata | 按 Host schema | 显式同名 prop 消费,否则进入 root attrs | 拒绝 | 按实际 TAG kind |
| declared Component prop | 不适用 | 接受 | 拒绝 | 按实际 TAG kind |
Component prop declaration 禁止 key、if、else-if、else、for。
directive 在业务 prop 验证前剥离;:key 单独进入 framework identity。剩余输入
只通过统一 Ebox/ETAF Host schema 判定 attrs,不能由组件库复制属性白名单。
7.2 单根 Host attrs 透传
Component 调用输入规范化为四个互不重叠的域:
declared props | Host attrs | :key | slots
attrs 遵守下面的不变量:
- 单一根 Host:attrs 应用到该 Host;
- 单一根 Component:attrs 沿根 Component 链继续传递;若下游显式声明同名 prop, 则由下游消费;
- string 根:先规范化为 Text,再应用 attrs;
- nil、slot projection 或 Fragment/多根结果:存在 attrs 时精确报错,不能猜目标;
class合并并去重;- Ebox style/layout attrs 先按 canonical property domain 去重,调用方值覆盖组件视觉 默认值;
role、ref、disabled、:on-*等 Runtime metadata 与普通前端 attrs 一样由 调用方覆盖根默认值;必须由 Component 掌控的 metadata 应显式声明为业务 prop, 从 attrs 域中消费;- attrs 在调用方 Component 环境中求值,不能泄漏到被调用 Component 的 setup、
etaf-state或业务 props; - 实际根 Host 仍通过
ebox-style-property-accepted-p验证适用域,例如 Text 根不能 接收 Box padding。
这条规则对应前端的 single-root fallthrough attribute,但 ETAF 只接受已注册的 Host schema 名称;任意未知 keyword 仍然是错误。
所有 Host、Component、Fragment、DSL :for item 与 etaf-node 使用同一 key
契约:key 必须是非 nil symbol、integer 或 string;string 在 author boundary
防御性复制;比较函数固定为 equal;float、mutable composite 和 duplicate key
在 candidate mutation 前报错。
8. Setup 与 State
8.1 Stateful DSL Component
(etaf-define-component counter (&key initial-value)
:setup
(etaf-ref (or initial-value 0))
:view
(column
(text
(expr
(number-to-string (etaf-value (etaf-state)))))
(box
:on-press
(let ((count (etaf-state)))
(lambda ()
(setf (etaf-value count)
(1+ (etaf-value count)))))
"Increment")))
8.2 Stateful code Component
(etaf-define-component counter (&key initial-value)
:setup
(etaf-ref (or initial-value 0))
:render
(let ((count (etaf-state)))
(etaf-node
'column nil
(list
(number-to-string (etaf-value count))
(etaf-node
'box
(list :on-press
(lambda ()
(setf (etaf-value count)
(1+ (etaf-value count)))))
(list "Increment"))))))
8.3 多个状态:返回一个显式 model
:setup 永远只返回一个值。Component 有多个 ref/computed/resource 时,把它们放进
一个普通 Elisp model;ETAF 不自动解构字段,也不注入字段名。
小型 Component 推荐使用 plist:
(etaf-define-component counter (&key initial-value)
:setup
(let ((count (etaf-ref (or initial-value 0)))
(step (etaf-ref 1)))
(list :count count :step step))
:view
(column
(text
(expr
(let ((count (plist-get (etaf-state) :count)))
(number-to-string (etaf-value count)))))
(box
:on-press
(let* ((model (etaf-state))
(count (plist-get model :count))
(step (plist-get model :step)))
(lambda ()
(setf (etaf-value count)
(+ (etaf-value count) (etaf-value step)))))
"Increment")))
完整数据流是:
:setup
→ 返回 (:count COUNT-REF :step STEP-REF)
→ etaf-state 取得整个 model
→ plist-get 取得指定 ref
→ etaf-value 取得 ref 当前值
复杂或复用的 Component 可以主动定义 struct,但定义必须与使用一起展示:
(cl-defstruct
(counter-model (:constructor counter-model-create))
count
step)
(etaf-define-component counter (&key initial-value)
:setup
(counter-model-create
:count (etaf-ref (or initial-value 0))
:step (etaf-ref 1))
:render
(let* ((model (etaf-state))
(count (counter-model-count model))
(step (counter-model-step model)))
(etaf-node
'text nil
(list (format "%s (+%s)"
(etaf-value count)
(etaf-value step))))))
counter-model-create、counter-model-count、counter-model-step 明确由紧邻的
cl-defstruct 定义产生,不是 ETAF API。ETAF 不增加 (etaf-state :count) 之类的
plist-only shortcut,也不自动注入 count/step;这样 state 可以同样是 ref、plist、
struct、Data Controller、Resource 或应用自己的任意 model。
8.4 Setup 语义
- setup 是普通 Elisp form,不是第二种 DSL;
- 每个 retained Component identity 只运行一次;
- setup 的返回值就是该实例的 opaque state;
- Runtime 不解析、复制或按 plist 约定解释 state;
etaf-state在当前 Component 的:view/:render求值期间返回 setup 的准确结果;- Runtime 单独记录 Component 是否定义 setup;setup 合法返回 nil 时,
etaf-state成功返回 nil,不能把 nil 当作“没有 setup”的 sentinel; - 没有 setup 或在 Component render context 之外调用
etaf-state会精确报错; - ETAF 不注入裸
statevariable,也不按返回对象的字段名自动注入变量; - prop 更新不重新执行 setup;
- setup 中直接读取的 prop 是初始化 snapshot,普通 lexical closure 也保留这个初始值;
- setup callback 不会在未来执行时隐式恢复“当前 prop 环境”;
- 需要当前 prop 的 event callback 应在每次
:view/:render中显式闭包当前值,或 调用 state 中接受当前值作为参数的 action; - setup 不读取或保留 slot projection;slots 只在
:view/:render阶段读取; - setup 失败时不创建可见实例;候选失败时释放候选 Scope;
- setup 不返回 render function,不包含
etaf-view,也不调用etaf-node构造结构。
最简单的 state 可以直接是一个 ref,如上例。需要多个值时可以返回普通 plist 或
用户自己明确定义的 cl-defstruct;若使用 struct,文档必须同时展示它的定义,并
说明 constructor/accessor 来自该定义,不能把生成函数当作不言自明的框架 API。
reactive value 放在 etaf-ref/etaf-computed 中。Runtime 不要求某一种业务数据结构。
9. Reactivity 与 Scope
Component setup 可以组合现有响应式能力:
| 能力 | API |
|---|---|
| 可写状态 | etaf-ref、etaf-value、setf/etaf-set-value |
| 派生状态 | etaf-computed |
| 精确观察 | etaf-watch |
| 自动依赖 Effect | etaf-watch-effect |
| 子 Scope | etaf-effect-scope、etaf-scope-run、etaf-scope-stop |
| cleanup | etaf-on-scope-dispose |
不变量:
- reactive source 有稳定 owner;
- render 只订阅实际读取的 source;
- 一次 Action/event 中的多次写入合并为一个 Runtime batch;
- Component 被成功移除后停止其 Scope;
- candidate 回滚不能停止仍属于旧 generation 的 Scope;
- setup、watch、effect 与 resource cleanup 各执行准确一次。
10. Children 与 Slots
10.1 默认 slot
调用方 trailing children 自动进入 default slot:
(panel
(text "Body"))
Component DSL 中投影:
(box (slot))
带 fallback:
(box (slot (text "Empty")))
10.2 Named slot
调用方:
(panel
(slot :name 'header
(text "Books"))
(slot :name 'actions
(box :on-press on-add "Add"))
(text "Body"))
Component:
(column
(slot :name 'header)
(slot)
(slot :name 'actions))
代码模式调用 Component 时,etaf-node 的第四个参数传 named-slot alist;代码模式
读取当前 slots 时使用 etaf-current-slot/etaf-current-slots,返回规范 child list。
Slot 规则:
- name 是 quoted、稳定、非 keyword symbol;
- 默认 slot 不能同时由 trailing children 和显式 named input 提供;
- 同名 slot input 不能重复;
- slot 内容的语义 owner 是调用方,projection 位置属于接收方;
- projection 移动、Component 更新与 rollback 不能改变 caller-owned identity;
- slot 不是 Box,不引入布局。
11. Styles、class 与 id
(etaf-define-component card (&key title)
:view
(column :class '(card)
(text :class '(card__title) (expr title))
(slot))
:styles
(styles
(".card" :padding 1 :background-color "#20242a")
(".card__title" :font-weight bold)))
契约:
:styles是 definition-owned static source;- selector/cascade/inheritance 只由 ECSS 运行一次;
:class/:id是 selector source metadata,不是 CSS declaration;- inline style、Component stylesheet、Theme/state contribution 最终进入同一 computed property/projection pipeline;
- Ebox/TP 不重新运行第二次 cascade;
- alias 在 parse 时归一化,后端只看到 canonical property ID;
etaf-component-set-styles只服务显式 authoring/hot-reload 工具,不隐式刷新全部 Runtime。
12. Events、Action 与 Behavior
12.1 局部 callback
Host 的 :on-* metadata 接受 function:
(box :ref 'save-button
:on-press (lambda () (save-document document))
"Save")
callback 属于当前 Runtime/Component identity,由 etaf-dispatch-event、
etaf-activate 或对应输入适配器调用。它不进入 ECSS 或 Ebox layout property。
12.2 Action
跨 Component 的命名业务变更使用:
etaf-action-define
etaf-action-register
etaf-dispatch
etaf-action-undefine
Action 是变更边界,不是 Component、Host 或 directive。
12.3 Behavior
可复用交互属性 bundle 使用:
etaf-define-behavior
etaf-behavior-create
etaf-focusable
etaf-toggleable
Host 通过 :use 安装 Behavior。Behavior 可以组合 callback、ref、focus 等语义,
不能创建第二套 Widget Runtime。
13. Context 与 Theme
setup 中通过:
etaf-provide / etaf-inject
etaf-theme-provide
etaf-theme-defaults
etaf-theme-value
etaf-theme-token
组合非视觉依赖和 Theme token。
规则:
- Context 沿 Component identity tree 继承;
- provider 属于一个 retained Component Scope;
- Theme 是 Context 中的语义值,不是 TP palette 的业务别名;
etaf-theme-tp只是可选 palette adapter;- View/render 读取 Theme 形成依赖;
- Theme 更新只影响实际读取 token/property 的 owner;
- 不因为 Theme 改变而默认重跑所有 Component。
14. Lifecycle
setup 中注册:
etaf-on-mounted
etaf-on-updated
etaf-on-unmounted
etaf-on-scope-dispose
精确顺序:
candidate setup
→ candidate render/lower
→ Ebox/TP 原子提交
→ generation promotion
→ mounted/updated callback
移除时:
successful replacement/removal
→ unmounted
→ Scope/resource cleanup
失败 candidate 不触发旧实例 unmounted;observer、updated callback 或应用 callback 不能取得 transaction commit 权限。生命周期 callback 不在纯 render 中注册。
Lifecycle hook 发生在 generation promotion 之后,因此 hook error 不能回滚已经提交
的 buffer/generation。mounted/updated hook error 被 Runtime error reporter 包含并继续
执行其余 postcommit 工作;unmounted hook error 也被包含,Scope/Resource cleanup
始终通过 unwind-protect 完成,不能被 hook 中断。
15. Resource、Data 与存储
Component 本身不实现数据库或异步调度。setup 只组合 owner 已明确的能力:
etaf-resource:Component Scope 所有的 load/value/error/cleanup;etaf-data-controller:query/page/items/selection/mutation 状态;etaf-sqlite或未来etaf-dbprovider:I/O 与事务;- Component View/render:读取状态并显示 UI。
典型 state:
(cl-defstruct shelf-state data selected-book)
setup 创建 controller/resource 并把它放入 state;DSL/代码 render 通过公共 accessor 读取。unmount 由 Scope/owner cleanup,不由 View node 自己关闭数据库。
第一版不增加 :await、Resource directive、Promise node 或数据库专用 Component。
异步 Request、cancel 和 stale-result 语义必须先在 Resource/Data owner 中冻结,再决定
是否需要新的作者语法。
16. Error 与回滚
16.1 Precommit failure
Component 的 setup、render、directive、property expression、lowering 或 Ebox/TP precommit 失败时:
- 当前成功 generation、buffer 文本、文本属性和 identity 保持不变;
- candidate 中新建的 Scope/Resource 被释放;
- 旧 arm/list/order 继续有效;
- framework publish/rollback 仍由 Runtime/Ebox/TP transaction participant 控制;
- observer 只读,不能授权 commit;
- 失败不能通过退化成 full-root、跳过属性或吞掉 lifecycle 来“恢复”。
16.2 Postcommit failure
mounted/updated/unmounted hook 和只读 observer 在 promotion 后运行。它们的 error:
- 不回滚已经成功的 generation;
- 不阻止其余 hook、observer 或 cleanup;
- 进入统一 Runtime diagnostic/error reporter;
- 不获得重新发布或修改 transaction decision 的权限。
event/Action callback 的 mutation 与 error 原子性由 Runtime Action transaction 契约 单独定义,不能与 lifecycle postcommit error 混为一类。
etaf-error-boundary-run 只是同步函数调用边界。它不是结构性 subtree boundary;在
candidate checkpoint、fallback failure、cleanup 和 retry identity 没有独立证明前,
不伪装成模板 error-boundary directive。
17. Root 使用与 mount
DSL root:
(etaf-mount
"*books*"
(etaf-view
(books-app :database database)))
代码 root:
(etaf-mount
"*books*"
(etaf-node 'books-app
(list :database database)
nil))
卸载:
(etaf-unmount runtime)
.etaf、Playground、未来 SFC compiler 或普通 .el 只能生成这两个公共入口之一,
不能创建第二套文件 Runtime、companion execution protocol 或隐式 mount 语义。
18. 编译、IR 与性能
18.1 DSL
etaf-view/:view 在 macro/compiler boundary:
- 解析 tag、attrs、slots、directive 和 interpolation;
- 生成 versioned
etaf-view-blueprint/2; - 静态结构提前验证;
- directive lowering 为已有 Branch/KeyedSequence/Range block;
- Runtime 只填动态洞并验证 ABI。
18.2 代码模式
:render 是普通 Elisp;每次需要 render 时运行用户代码,etaf-node 立即构造同一
ABI 的 typed View value。Runtime 不 eval raw list,也不把它重新送进 DSL compiler。
18.3 共同要求
- 两种模式共享一个 bounded IR validator;
- 同一输入只 materialize 一次;
- 下游保留 stable identity、dependency、Range、layout proof 和 paint contribution;
- DSL 静态 blueprint 与代码 View value 进入同一 retained reconciliation;
- keyed list 允许一次 O(n) key/order certification,下游为 O(changed+moves);
- 不允许每层重新扫描 O(all);
- 旧 ABI、旧 builder 或 stale
.elc/.eln必须给出 clean-rebuild error,不能静默兼容; - 两种模式都必须通过真实 GUI p95/max ≤ 50ms 门禁。
19. 严格禁止的混写
下面全部是定义错误:
;; 同时选择两个 frontend
:view (column ...)
:render (etaf-node 'column nil nil)
;; setup 返回 render closure
:setup (lambda () (etaf-view (column ...)))
;; :render 定义边界直接嵌 DSL source
:render (etaf-view (column ...))
;; DSL child 里直接写 Elisp control
:view (column (mapcar #'make-row books))
;; expr 返回结构
:view (column (expr (etaf-node 'book-row nil nil)))
;; etaf-node 使用 directive
(etaf-node 'book-row
(list :for (list 'book books))
nil)
;; 代码 render 返回 raw node list
:render (mapcar #'make-row books)
这些错误不能通过 runtime 猜测、quote 规则、兼容 wrapper 或 load order 自动修复。 普通 helper 返回的已验证 typed View 可以在代码模式中复用;这不是第二套 author grammar,也不要求 IR 保存 frontend provenance。
20. 两种模式的完整对照
| 问题 | DSL :view |
代码 :render |
|---|---|---|
| node | (box ...) |
(etaf-node 'box PROPS CHILDREN) |
| Component call | (book-row :book book) |
(etaf-node 'book-row (list :book book) nil) |
| 条件 | :if/:else-if/:else |
if/when/cond |
| 循环 | :for (item items) + :key |
mapcar/cl-loop + node :key |
| 动态文本 | (expr FORM) |
普通 string expression |
| 多根 | (fragment ...) |
(etaf-node 'fragment nil nodes) |
| 默认 slot 输入 | trailing children | CHILDREN 参数 |
| named slot 输入 | (slot :name 'x ...) |
NAMED-SLOTS 参数 |
| slot projection | (slot ...) |
etaf-current-slot |
| property value | 普通 Elisp expression | 已求值 plist value |
| quote | node 不 quote;value 按 Elisp | 全部按普通 Elisp |
| tag resolution | Component/Host Registry | Component/Host Registry |
| Elisp function resolution | 仅 value/expr/callback | 普通函数位置 |
| topology compiler | 是 | 否 |
| Runtime/identity/lifecycle | 同一套 | 同一套 |
21. 历史实现快照(非当前状态)
下表只记录本文创建时的历史差距,不能用于判断当前版本。当前实现与迁移状态见 implementation-plan.zh.md。
| 能力 | 历史状态 | 当时目标动作 |
|---|---|---|
etaf-define-component :view |
已实现 | 按新 DSL grammar 收敛。 |
:setup |
已实现,但返回零参数 render function | 删除旧语义;改为只返回 state。 |
etaf-state |
未实现 | 新增 setup result 的唯一显式 accessor。 |
:render |
未实现 | 新增严格代码 frontend。 |
etaf-node |
未实现 | 新增唯一普通函数节点构造器。 |
(expr :value FORM) |
当前实现 | 删除;只保留 (expr FORM) 文本插值。 |
:if/:else-if/:else/:for |
未实现 | 由 compiler lowering,Runtime 不接收 directive。 |
| Component alias | 当前会参考 fboundp 自动生成 |
删除 load-order-dependent alias;只用准确 registry name。 |
| 单根 Host attrs | 已实现最小纵向切片 | 继续迁移 etaf-ui,删除重复视觉 props 声明。 |
| Host | 当前已有 text/box/fragment/row/column/flex/grid | 保持最小闭集。 |
| props/slots/styles/reactivity/lifecycle | 已有主体能力 | 迁移到共同 ComponentDefinition,不复制实现。 |
这是一次 clean migration,不保留以下 compatibility:
- setup-return-render;
(expr :value ...);- 结构 Expr;
- load-order-dependent Component alias;
- raw quoted View list;
:render定义边界中的直接 DSL wrapper;- DSL 中的任意 direct-child Elisp。
22. 落地门禁
实现只有同时满足下面条件才算完成:
- definition parser 精确拒绝混写、未知 clause、重复/保留 prop;
- DSL 与代码模式生成同一版本 typed View IR;
- 同名 Elisp function/variable 的新增、删除和加载顺序不改变 tag 语义;
- props、single-root attrs、default/named slot、styles、Context、Theme、events、Behavior 全部等价;多根 attrs 精确拒绝;
- setup-once、prop update、mounted/updated/unmounted、Scope cleanup 等价;
- branch 与 keyed list 的 insert/delete/reorder 保留正确 identity;
- injected failure 完整恢复 generation、文本、文本属性、Range 与 lifecycle;
- stale bytecode/IR 只有 clean-rebuild error,没有兼容成功路径;
- Research Shelf 同时用 DSL 与至少一个代码 Component 证明真实组合能力;
research-shelf.etaf、flex-reference.ebox、grid-reference.eboxGUI 无告警、 渲染正确;- resize、scroll、row、page、filter、theme、add、reload 的固定场景 p95 与 max 都不超过 50ms;
- 文档、tests、byte/native compile、dependency boundaries 全部通过后才迁入正式 API reference。
23. 最终决策摘要
一个 Component 模型
├── DSL frontend: :view + tag/directive/expr
└── Code frontend: :render + ordinary Elisp + etaf-node
共享:props + setup/etaf-state + slots + styles + reactive Scope
+ Context/Theme + events/Action/Behavior + lifecycle
+ Resource/Data + identity/Range + atomic rollback
核心约束是:
DSL 负责声明结构;代码模式负责用普通 Elisp计算结构;两者都只产生同一种 typed View,不互相嵌套,不通过猜测跨越边界。