From b82e94a86dfb676ffd967ce6677701fa12b90fc7 Mon Sep 17 00:00:00 2001 From: Kinneyzhang Date: Thu, 27 Aug 2026 23:23:29 +0800 Subject: [PATCH] docs: define dual component authoring frontends --- docs/proposals/component-definition.zh.md | 1051 +++++++++++++++++++++ docs/proposals/module-boundaries.zh.md | 22 +- 2 files changed, 1067 insertions(+), 6 deletions(-) create mode 100644 docs/proposals/component-definition.zh.md diff --git a/docs/proposals/component-definition.zh.md b/docs/proposals/component-definition.zh.md new file mode 100644 index 0000000..c579534 --- /dev/null +++ b/docs/proposals/component-definition.zh.md @@ -0,0 +1,1051 @@ +# ETAF Component 定义:目标设计 + +> 状态:待审查提案,尚未全部实现。 +> +> 本文只定义目标公共契约,不代表当前版本已经支持这些写法。当前实现差距见 +> “实现状态与迁移”一节。审查通过并完成代码、测试、GUI 与性能门禁后,稳定内容 +> 才能进入用户指南。 + +本文只拥有 **Component 作者语法和作者可见语义**。属性 owner、跨包依赖、 +transaction 权限、Ebox/TP/Rust 边界仍以 +[module-boundaries.zh.md](module-boundaries.zh.md) 为准;本文只引用,不复制第二套 +系统规范。本文的 DSL 选择取代旧 capability-expansion 草案中的 Structural Expr +方向;对应计划和测试规格必须原子更新后才能实施。 + +## 1. 目标 + +ETAF 只保留一种 Component 语义模型,但允许用户用两种严格分开的方式定义它: + +1. **DSL 模式**:类似 Vue template,结构由 tag、attribute、slot 和少量编译指令表达; +2. **代码模式**:类似 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. 最小心智模型 + +用户只需先理解五个概念: + +1. **Component**:可复用、可保留状态和生命周期的语义单元; +2. **Host**:真正参与文本、盒模型和布局的视觉节点; +3. **props**:父级传入的只读输入; +4. **state**:Component 实例自己保留的状态; +5. **children/slots**:父级交给 Component 的子内容。 + +然后二选一: + +```text +喜欢声明式模板 → :view DSL +喜欢完整 Elisp → :render + etaf-node +``` + +普通用户不需要理解 Ebox node id、TP object、buffer text property、compiler block、 +Runtime generation 或 Rust tape。 + +## 3. 唯一 Component 定义外壳 + +两种模式都使用同一个定义入口: + +```elisp +(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 在定义时直接报错; +- 不存在根据返回值猜测定义模式的行为。 + +推荐固定按下面的顺序书写: + +```text +docstring → :setup → :view/:render → :styles +``` + +解析器可以按 keyword 识别 clause,但正式示例只展示这一种顺序。 + +## 4. Component 名称与命名空间 + +Component tag 属于 ETAF Component Registry,不属于 Elisp function namespace。 + +```elisp +(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 基本定义 + +```elisp +(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 的唯一结构 + +```elisp +(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 + +```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: + +```elisp +(box "Hello") +``` + +等价于没有额外 Text 属性的: + +```elisp +(box (text "Hello")) +``` + +需要 color、font、ref 或其他 Text 能力时显式写 `text`。`fragment` 不参与布局, +不伪装成 Box,也不引入公开 `flow`、`spacer`、`item` 或 `grid-item` 节点。 + +视觉属性的完整 schema、适用 Host、alias、shorthand 和 ParentParticipation 由 +[module-boundaries.zh.md](module-boundaries.zh.md) 的统一 property schema 单独定义; +本文不复制第二份属性表。 + +### 5.5 文本插值:`expr` + +DSL 中唯一的动态文本桥接是: + +```elisp +(expr FORM) +``` + +例如: + +```elisp +(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: + +```text +:if +:else-if +:else +:for +``` + +以及已有 identity metadata: + +```text +:key +``` + +#### 条件 + +```elisp +(column + (book-detail :if selected-book :book selected-book) + (empty-detail :else t)) +``` + +```elisp +(column + (loading-view :if loading-p) + (error-view :else-if error :error error) + (book-list :else t :books books)) +``` + +规则: + +- `:else` 的值必须是 literal `t`; +- `:else-if`/`:else` 必须直接相邻地跟在同级 `:if` 后; +- chain 不能跨 Fragment、Component、slot 或 opaque boundary; +- 缺少最终 `:else` 等价于空 arm; +- branch 切换只有在新 generation 成功后才释放旧 arm; +- 失败时恢复原 arm、identity、文本与属性。 + +#### 循环 + +```elisp +(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: + +```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 唯一节点构造函数 + +代码模式只用一个公共节点构造函数: + +```elisp +(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。 + +示例: + +```elisp +(etaf-node 'box + (list :padding 1 :background-color "#20242a") + (list "Hello")) +``` + +```elisp +(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: + +```elisp +(etaf-node 'fragment nil nodes) +``` + +不允许 render body 直接返回一个 node list,因为“一个 View”与“多个 siblings”必须 +有不同类型,不能再次依靠 list 形状猜测。 + +### 6.3 Render 结果 + +`:render` 只能返回: + +```text +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 + +```elisp +(etaf-define-component user-card (&key user compact on-open) + ...) +``` + +规则: + +- 声明只接受普通 symbol; +- 每个 prop 对应同名 keyword,例如 `user` 对应 `:user`; +- 未传 prop 的值为 nil; +- 未声明 prop、重复 prop、奇数 plist 在调用边界报错; +- props 在一次 render 中是只读 snapshot; +- prop 更新不重新运行 setup,只触发依赖它的 render/patch; +- `:key` 和 directive 不会进入 Component 业务 props; +- Host 的 `:ref`、`:class`、`:id`、`:on-*`、`:use` 由 Host schema 解释;Component + 只有显式声明同名 prop 时才接收对应 keyword,不做隐式 forwarding; +- 第一版不增加另一套 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 同名时才是业务 prop | 拒绝 | 按实际 TAG kind | +| declared Component prop | 不适用 | 接受 | 拒绝 | 按实际 TAG kind | + +Component prop declaration 禁止 `key`、`if`、`else-if`、`else`、`for`。 +directive 在业务 prop 验证前剥离;`:key` 在 Component 调用前剥离。除此之外不按 +keyword 名称猜测 forwarding。 + +所有 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 + +```elisp +(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 + +```elisp +(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: + +```elisp +(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"))) +``` + +完整数据流是: + +```text +:setup +→ 返回 (:count COUNT-REF :step STEP-REF) +→ etaf-state 取得整个 model +→ plist-get 取得指定 ref +→ etaf-value 取得 ref 当前值 +``` + +复杂或复用的 Component 可以主动定义 struct,但定义必须与使用一起展示: + +```elisp +(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 不注入裸 `state` variable,也不按返回对象的字段名自动注入变量; +- 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: + +```elisp +(panel + (text "Body")) +``` + +Component DSL 中投影: + +```elisp +(box (slot)) +``` + +带 fallback: + +```elisp +(box (slot (text "Empty"))) +``` + +### 10.2 Named slot + +调用方: + +```elisp +(panel + (slot :name 'header + (text "Books")) + (slot :name 'actions + (box :on-press on-add "Add")) + (text "Body")) +``` + +Component: + +```elisp +(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 + +```elisp +(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: + +```elisp +(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 的命名业务变更使用: + +```text +etaf-action-define +etaf-action-register +etaf-dispatch +etaf-action-undefine +``` + +Action 是变更边界,不是 Component、Host 或 directive。 + +### 12.3 Behavior + +可复用交互属性 bundle 使用: + +```text +etaf-define-behavior +etaf-behavior-create +etaf-focusable +etaf-toggleable +``` + +Host 通过 `:use` 安装 Behavior。Behavior 可以组合 callback、ref、focus 等语义, +不能创建第二套 Widget Runtime。 + +## 13. Context 与 Theme + +setup 中通过: + +```text +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 中注册: + +```text +etaf-on-mounted +etaf-on-updated +etaf-on-unmounted +etaf-on-scope-dispose +``` + +精确顺序: + +```text +candidate setup +→ candidate render/lower +→ Ebox/TP 原子提交 +→ generation promotion +→ mounted/updated callback +``` + +移除时: + +```text +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-db` provider:I/O 与事务; +- Component View/render:读取状态并显示 UI。 + +典型 state: + +```elisp +(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: + +```elisp +(etaf-mount + "*books*" + (etaf-view + (books-app :database database))) +``` + +代码 root: + +```elisp +(etaf-mount + "*books*" + (etaf-node 'books-app + (list :database database) + nil)) +``` + +卸载: + +```elisp +(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. 严格禁止的混写 + +下面全部是定义错误: + +```elisp +;; 同时选择两个 frontend +:view (column ...) +:render (etaf-node 'column nil nil) +``` + +```elisp +;; setup 返回 render closure +:setup (lambda () (etaf-view (column ...))) +``` + +```elisp +;; :render 定义边界直接嵌 DSL source +:render (etaf-view (column ...)) +``` + +```elisp +;; DSL child 里直接写 Elisp control +:view (column (mapcar #'make-row books)) +``` + +```elisp +;; expr 返回结构 +:view (column (expr (etaf-node 'book-row nil nil))) +``` + +```elisp +;; etaf-node 使用 directive +(etaf-node 'book-row + (list :for (list 'book books)) + nil) +``` + +```elisp +;; 代码 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. 实现状态与迁移 + +截至本文创建时,当前代码的真实状态是: + +| 能力 | 当前状态 | 目标动作 | +| --- | --- | --- | +| `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 | 当前已有 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. 落地门禁 + +实现只有同时满足下面条件才算完成: + +1. definition parser 精确拒绝混写、未知 clause、重复/保留 prop; +2. DSL 与代码模式生成同一版本 typed View IR; +3. 同名 Elisp function/variable 的新增、删除和加载顺序不改变 tag 语义; +4. props、default/named slot、styles、Context、Theme、events、Behavior 全部等价; +5. setup-once、prop update、mounted/updated/unmounted、Scope cleanup 等价; +6. branch 与 keyed list 的 insert/delete/reorder 保留正确 identity; +7. injected failure 完整恢复 generation、文本、文本属性、Range 与 lifecycle; +8. stale bytecode/IR 只有 clean-rebuild error,没有兼容成功路径; +9. Research Shelf 同时用 DSL 与至少一个代码 Component 证明真实组合能力; +10. `research-shelf.etaf`、`flex-reference.ebox`、`grid-reference.ebox` GUI 无告警、 + 渲染正确; +11. resize、scroll、row、page、filter、theme、add、reload 的固定场景 p95 与 max + 都不超过 50ms; +12. 文档、tests、byte/native compile、dependency boundaries 全部通过后才迁入正式 + API reference。 + +## 23. 最终决策摘要 + +```text +一个 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,不互相嵌套,不通过猜测跨越边界。 diff --git a/docs/proposals/module-boundaries.zh.md b/docs/proposals/module-boundaries.zh.md index 842cdfc..7d1e668 100644 --- a/docs/proposals/module-boundaries.zh.md +++ b/docs/proposals/module-boundaries.zh.md @@ -26,6 +26,10 @@ Text、Box、Component;Fragment、布局细节和底层包通过渐进披露 (grid PROPS CHILD...) ``` +Component 只有一个语义模型,但有两个互斥的定义 frontend:`:view` 使用上述 DSL, +`:render` 使用普通 Elisp 和唯一函数 `etaf-node`。完整作者契约见 +[component-definition.zh.md](component-definition.zh.md)。 + `row/column/flex/grid` 都只是 Box 的布局形式。用户属性分为 metadata、语义/事件、 视觉样式和 parent participation;采用语义一致的标准 CSS 名称、少量明确的 ETAF/Ebox 个性属性和少量 parse-time alias。用户不接触 Ebox/TP/Emacs backend。 @@ -146,7 +150,8 @@ AuthorNode = String `String` 规范化为 Text;五个 Box form 规范化为带对应 typed Layout 的 Box。form 名称不进入 Runtime identity、diff、style、layout 或 paint 协议。ETAF 另外增加 Fragment、 -ComponentCall、`expr` 和 `slot`;Ebox DSL 不拥有这些语义。 +ComponentCall、文本插值 `expr`、slot 和结构 directive;Ebox DSL 不拥有这些语义。 +代码 frontend 的 `etaf-node` 直接构造同一种 typed View,不重新解析 DSL。 两层作者语法共享结构词汇和规范化结果,不要求共享求值环境:ETAF property 是 Elisp expression;`.ebox` DSL 是数据 form。quote 和 expression 的差异只属于 @@ -156,7 +161,9 @@ Flex/Grid item 是 material child Box 在父 formatting context 中获得的角 另一种 View 或父子边类型。participation 属性存储在 child Box,合法性、计算和 dirty propagation 由直接父 Flex/Grid Box 拥有。 -`expr` 和 `slot` 是计算/投影机制,不是视觉节点。目标公共 View 不接受原始 Ebox +`expr` 和 `slot` 是文本计算/投影机制,不是视觉节点。DSL 结构变化由 +`:if/:else-if/:else/:for` 表达;代码 frontend 使用普通 Elisp control。目标公共 View +不接受原始 Ebox 节点;缺失的渲染能力必须先形成有类型、有 identity/impact 契约的 View/Ebox 能力, 不能通过 opaque escape 绕过框架语义。 @@ -179,7 +186,7 @@ dirty propagation 由直接父 Flex/Grid Box 拥有。 ```text ETAF TextForm = (text TEXT-PROP VALUE) -VALUE = String | (expr :value Expr) +VALUE = String | (expr Expr) Ebox TextForm = (text TEXT-PROP String) ``` @@ -218,7 +225,8 @@ Text :fixed inline 因此 inline-flex 写为 `(flex :outer 'inline ...)`,不是新节点类型。`:outer` 是所有 Box author form 的公共属性;layout config 由对应 form 的闭集属性构造。需要动态 -切换 layout 时,由 `expr` 选择不同 View form,而不是把 `:layout` 当普通动态样式。 +切换 layout 时,DSL 由结构 directive 选择不同 View form,代码 frontend 由普通 +Elisp control 选择不同 `etaf-node` tag;不能把 `:layout` 当普通动态样式。 Canonical Box 使用 typed Layout variant;不公开 `:inner` 或 Ebox `(:display (block flow))` 后端表示。 @@ -745,7 +753,9 @@ Elisp plist。调试与性能工具必须能显示完整链路,例如: `etaf-component`: -- 定义 Component props、`:view`、`:setup`、`:styles`; +- 定义 Component props、互斥的 `:view`/`:render`、返回 opaque state 的 `:setup`、 + 唯一显式 accessor `etaf-state` 和 `:styles`; +- 定义 `etaf-node` 代码 frontend 与同一 typed View ABI 的边界; - 不调度 Runtime,不调用 Ebox。 `etaf-runtime`: @@ -871,7 +881,7 @@ Playground 的 `.etaf` manifest 属于 Playground。未来通用 compiler 若存 ### 4.6 按使用者分层的公共面 -- 普通 ETAF 用户只使用 `etaf-view`、Component、Text/Box form、ETAF metadata/ +- 普通 ETAF 用户只使用 `etaf-view` 或 `etaf-node`、Component、Text/Box form、ETAF metadata/ events、canonical CSS-aligned properties,以及明确列出的 ETAF/Ebox extensions (如 `:outer`、`:wrap-mode`)和 parse-time aliases;不调用 Ebox/TP/Emacs adapter。 - 独立 Ebox 用户通过 `ebox-build` 使用同一视觉结构词汇的 inert data form;不拥有