Implement retained ETAF reactive runtime

This commit is contained in:
Kinneyzhang 2026-08-22 06:19:00 +08:00
parent 30aa141cca
commit abb947e0bc
15 changed files with 8157 additions and 280 deletions

View File

@ -1,5 +1,5 @@
EMACS ?= emacs EMACS ?= emacs
LOAD_PATH = -L . -L examples -L ../ebox LOAD_PATH = -L . -L examples -L ../ebox -L ../tp -L ../ecss
SOURCES = etaf-view.el etaf-component.el etaf-reactive.el etaf-context.el etaf-resource.el etaf-data.el etaf-renderer.el etaf-runtime.el etaf-behavior.el etaf-actions.el etaf-events.el etaf.el SOURCES = etaf-view.el etaf-component.el etaf-reactive.el etaf-context.el etaf-resource.el etaf-data.el etaf-renderer.el etaf-runtime.el etaf-behavior.el etaf-actions.el etaf-events.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-resource-tests.el tests/etaf-data-tests.el tests/etaf-examples-tests.el TESTS = tests/etaf-tests.el tests/etaf-resource-tests.el tests/etaf-data-tests.el tests/etaf-examples-tests.el

View File

@ -109,6 +109,8 @@ Attribute values do not need an `expr` wrapper. `expr` exists only because the c
It evaluates the expression, then accepts a string, View, sequence, or `nil`. It creates no Ebox wrapper, identity, lifecycle, watcher, or effect. `if`, `when`, `cond`, `let`, `mapcar`, `cl-loop`, and other Elisp forms remain ordinary Elisp inside `:value`. It evaluates the expression, then accepts a string, View, sequence, or `nil`. It creates no Ebox wrapper, identity, lifecycle, watcher, or effect. `if`, `when`, `cond`, `let`, `mapcar`, `cl-loop`, and other Elisp forms remain ordinary Elisp inside `:value`.
The current core has no file-facing `.etaf` pair loader. `etaf-define-component` is the structure/style/behavior unit: its View defines structure, `:styles` owns presentation rules, and `:setup` owns retained state, events, and lifecycle behavior. A future `.etaf` single-file component format belongs in a compiler layer that lowers into this same public View and Component contract; it is not a second runtime grammar.
## 4. Components ## 4. Components
The public definition macro has exactly three keywords: The public definition macro has exactly three keywords:
@ -127,6 +129,8 @@ The public definition macro has exactly three keywords:
`:view` and `:setup` are mutually exclusive. `:styles` is optional and may appear once. Props are the only declared business inputs; ordinary trailing children and named slots are normalized separately into the Component's slot collection. `:view` and `:setup` are mutually exclusive. `:styles` is optional and may appear once. Props are the only declared business inputs; ordinary trailing children and named slots are normalized separately into the Component's slot collection.
The Component definition is the current structure/style/behavior boundary. Keep dynamic state, Action callbacks, and lifecycle work in `:setup`; keep static presentation in `:styles`. A future `.etaf` SFC compiler may produce these definitions, but the Runtime does not load `.etaf` files directly.
### 4.1 Stateless and stateful forms ### 4.1 Stateless and stateful forms
```elisp ```elisp
@ -342,6 +346,8 @@ Keys are stable ordinary symbols. The nearest ancestor wins; a missing required
Explicit Host props override Component styles, and Component styles override Theme defaults. Explicit Host props override Component styles, and Component styles override Theme defaults.
Style ownership follows the Component that authored each View node. A nested Component is a style boundary: parent rules do not penetrate its internals. Caller-provided slot content is rendered with the caller scope, while a child's own fallback content remains in the child scope.
### 9.2 Data ### 9.2 Data
Data is a core ETAF capability, not a second framework package. A Data Source is a small capability plist: Data is a core ETAF capability, not a second framework package. A Data Source is a small capability plist:
@ -417,7 +423,30 @@ etaf-playground → etaf
Optional storage integrations should use explicit package names such as a concrete SQLite or PostgreSQL source. A generic adapter package would add a name without owning a stable behavior, so it is not part of the public model. Optional storage integrations should use explicit package names such as a concrete SQLite or PostgreSQL source. A generic adapter package would add a name without owning a stable behavior, so it is not part of the public model.
## 11. Extension rule ## 11. Retained publication and fixed-point safety
Mounted Component, expr, slot, fragment, raw, inline, and Root owners publish
through one outer reactive dispatch. A local owner evaluates into a candidate
generation and Ebox logical replacement; disjoint owners are coalesced into
one TP/Ebox publication. The Root owner is the only complete-root adapter.
Each Runtime flush records a candidate-aware effect tuple containing the
generation id, effect-to-source edges and source versions, plus an immutable
semantic-node stamp for candidate input/context/output facts. A repeated tuple
reports the ordered effect/edge path of the non-converging graph. The flush
bound is derived from candidate nodes, dependency edges, and source entries;
it is not a fixed multiplier of an arbitrary threshold. Rollback discards the
flush-local stamps, so the same failed state can be retried as a new
transaction.
Behavior installers use target-specific identity: reactive values and
functions are compared with `eq`, while scalar attributes use value equality.
Each installed Behavior also receives a stable `(mount-epoch resource-id)`
address. Generation membership and the Runtime resource registry determine
which installer is authoritative; a failed candidate removes only its staged
Behavior resource and runs its contained cleanup.
## 12. Extension rule
Before adding a new concept, choose the smallest existing owner: Before adding a new concept, choose the smallest existing owner:

View File

@ -108,6 +108,8 @@ ATTRIBUTE = :KEY VALUE
它执行表达式然后接受字符串、View、View 序列或 `nil`。它不创建 Ebox wrapper、identity、生命周期、watch 或 effect。`if`、`when`、`cond`、`let`、`mapcar`、`cl-loop` 等 Elisp 形式仍然只是 `:value` 中的普通 Elisp。 它执行表达式然后接受字符串、View、View 序列或 `nil`。它不创建 Ebox wrapper、identity、生命周期、watch 或 effect。`if`、`when`、`cond`、`let`、`mapcar`、`cl-loop` 等 Elisp 形式仍然只是 `:value` 中的普通 Elisp。
当前 core 没有面向文件的 `.etaf` pair loader。`etaf-define-component` 才是结构/样式/行为单元View 定义结构,`:styles` 负责 presentation`:setup` 负责 retained state、事件和生命周期行为。未来的 `.etaf` SFC 属于把结果 lowering 到同一套公共 View/Component 契约的 compiler layer而不是第二套 Runtime 语法。
## 4. Component ## 4. Component
公共定义宏只有三个关键字: 公共定义宏只有三个关键字:
@ -126,6 +128,8 @@ ATTRIBUTE = :KEY VALUE
`:view``:setup` 互斥;`:styles` 可选且最多出现一次。Props 是唯一需要声明的业务输入;普通尾部子节点和命名 slot 会被规范化为 Component 的 slot 集合。 `:view``:setup` 互斥;`:styles` 可选且最多出现一次。Props 是唯一需要声明的业务输入;普通尾部子节点和命名 slot 会被规范化为 Component 的 slot 集合。
Component definition 是当前结构/样式/行为边界动态状态、Action callback 和生命周期工作放进 `:setup`,静态 presentation 放进 `:styles`。未来 `.etaf` SFC compiler 可以生成这些 definition但 Runtime 不会直接加载 `.etaf` 文件。
### 4.1 无状态与状态型形式 ### 4.1 无状态与状态型形式
```elisp ```elisp
@ -341,6 +345,8 @@ key 使用稳定的普通 symbol。最近的祖先优先缺失的 required ke
显式 Host props 覆盖 Component stylesComponent styles 覆盖 Theme defaults。 显式 Host props 覆盖 Component stylesComponent styles 覆盖 Theme defaults。
样式所有权跟随创建每个 View node 的 Component。嵌套 Component 是样式边界:父规则不会穿透其内部。调用者提供的 slot 内容使用 caller scope子组件自有的 fallback 内容保留在 child scope。
### 9.2 Data ### 9.2 Data
Data 是 ETAF core 能力不是需要用户额外学习的第二套框架。Data Source 是一个小的 capability plist Data 是 ETAF core 能力不是需要用户额外学习的第二套框架。Data Source 是一个小的 capability plist
@ -416,7 +422,27 @@ etaf-playground → etaf
可选存储集成应使用具体名称,例如 SQLite 或 PostgreSQL source。通用 adapter 包无法拥有稳定的行为,只会增加用户需要记忆的名称,因此不属于公共模型。 可选存储集成应使用具体名称,例如 SQLite 或 PostgreSQL source。通用 adapter 包无法拥有稳定的行为,只会增加用户需要记忆的名称,因此不属于公共模型。
## 11. 拓展规则 ## 11. 保留式发布与 fixed-point 安全
mounted Component、expr、slot、fragment、raw、inline 和 Root owner 都经过同一
个外层 reactive dispatch。局部 owner 先生成 candidate generation 和 Ebox logical
replacement不相交 owner 会合并为一次 TP/Ebox publication。只有 Root owner
可以进入 complete-root adapter。
每次 Runtime flush 都记录 candidate-aware effect tuple其中包含 generation id、
effect→source 边和 source version以及 candidate input/context/output facts 的
immutable semantic-node stamp。重复 tuple 会报告有序的 effect/edge pathstep
bound 从 candidate nodes、dependency edges 和 source entries 推导,不再是任意
固定倍率阈值。rollback 会丢弃本次 flush 的 stamp因此同一个失败状态可以作为
新的 transaction 重试。
Behavior installer 使用 target-specific identityreactive value 和 function 用
`eq`,普通 scalar attribute 才使用 value equality。每个已安装 Behavior 都获得
稳定的 `(mount-epoch resource-id)` 地址generation membership 与 Runtime resource
registry 决定当前 authority。失败 candidate 只移除 staged Behavior resource
执行受 containment 保护的 cleanup。
## 12. 拓展规则
增加新能力前,优先选择最小的既有 owner 增加新能力前,优先选择最小的既有 owner

View File

@ -58,6 +58,8 @@ Mount it into an Emacs buffer:
Use `etaf-mount` whenever a View contains a stateful Component, reactive data, events, or lifecycle. Use `etaf-mount` whenever a View contains a stateful Component, reactive data, events, or lifecycle.
The current core has no direct `.etaf` loader. `etaf-define-component` is the structure/style/behavior unit: use its View for structure, `:styles` for static presentation, and `:setup` for retained state, Actions, and lifecycle. A future `.etaf` SFC belongs to a compiler layer that emits this same Component contract; it is not a second Runtime entry point.
## 3. Properties and children ## 3. Properties and children
Attribute values are ordinary Elisp expressions. They do not need an extra `expr` wrapper: Attribute values are ordinary Elisp expressions. They do not need an extra `expr` wrapper:
@ -266,6 +268,14 @@ Precedence is fixed:
explicit Host properties > Component :styles > inherited Theme defaults explicit Host properties > Component :styles > inherited Theme defaults
``` ```
Within one Component style scope, the first matching declaration fills an
unspecified Host property; later rules do not overwrite that resolved default.
A non-nil Host property remains protected, and a nil Host property is treated
as omitted and may receive the Component or Theme default. Use explicit Host
props or distinct properties when a variant needs a deliberate override.
Styles are scoped to the Component that authored a View node. A parent rule does not enter a nested Component's internals; content supplied through a caller slot keeps the caller scope, while a Component's own slot fallback keeps the child scope.
Theme is a Context convenience, not another runtime object: Theme is a Context convenience, not another runtime object:
```elisp ```elisp
@ -339,7 +349,7 @@ For a reusable installer, reserve `:install` for the cleanup-producing part of t
(message "Behavior removed"))))))) (message "Behavior removed")))))))
``` ```
The installer can call `etaf-current-behavior-context` when it needs the current Runtime or Host path. Replacing the Behavior runs the old cleanup before the new state becomes current. The installer can call `etaf-current-behavior-context` when it needs the current Runtime or Host path. Replacing the Behavior runs the old cleanup before the new state becomes current. Behavior equality keeps function and reactive-value identity with `eq`; a newly-created installer closure is therefore a deliberate replacement, not an accidental reuse. The replacement is staged under the mounted resource registry and becomes authoritative only when its generation commits.
Focus and hit testing are Runtime operations: Focus and hit testing are Runtime operations:
@ -351,6 +361,8 @@ Focus and hit testing are Runtime operations:
`etaf-host-ref-bounds` and `etaf-host-ref-position` expose the public Ebox hit-test boundary. `etaf-dispatch-event` accepts an optional payload flag when the callback needs one argument. `etaf-host-ref-bounds` and `etaf-host-ref-position` expose the public Ebox hit-test boundary. `etaf-dispatch-event` accepts an optional payload flag when the callback needs one argument.
Mounted buffers enable `etaf-input-mode` automatically. `TAB` focuses the next Host, `Shift-TAB`/backtab focuses the previous Host, `RET` activates the focused Host, and `mouse-1` activates the Host at the click position. Focus ordering sorts numeric `:tab-index` first and uses live buffer position as the stable tie-breaker; moving focus also moves point to that Host. Unmounting disables the input mode.
## 9. Context / Provide / Inject ## 9. Context / Provide / Inject
Use Context for a dependency shared across component depth, not for ordinary props: Use Context for a dependency shared across component depth, not for ordinary props:
@ -563,7 +575,7 @@ It depends only on Ebox. Use it when inspecting Ebox boxes and Grid layout; use
| Reactive effects | `etaf-watch`, `etaf-watch-effect`, `etaf-effect-scope`, `etaf-scope-run` | Observe state or synchronize external resources | | Reactive effects | `etaf-watch`, `etaf-watch-effect`, `etaf-effect-scope`, `etaf-scope-run` | Observe state or synchronize external resources |
| Lifecycle | `etaf-on-mounted`, `etaf-on-updated`, `etaf-on-unmounted` | Attach Component lifecycle work | | Lifecycle | `etaf-on-mounted`, `etaf-on-updated`, `etaf-on-unmounted` | Attach Component lifecycle work |
| Context | `etaf-provide`, `etaf-inject`, `etaf-theme-provide` | Share ambient dependencies through depth | | Context | `etaf-provide`, `etaf-inject`, `etaf-theme-provide` | Share ambient dependencies through depth |
| Events and focus | `etaf-dispatch-event`, `etaf-activate`, `etaf-focus`, `etaf-focus-next` | Enter interactive Runtime behavior | | Events and focus | `etaf-dispatch-event`, `etaf-activate`, `etaf-focus`, `etaf-focus-next`, `etaf-focus-previous`, `etaf-input-mode` | Enter interactive Runtime behavior |
| Actions | `etaf-action-define`, `etaf-dispatch` | Name and reuse business mutations | | Actions | `etaf-action-define`, `etaf-dispatch` | Name and reuse business mutations |
| Behaviors | `etaf-behavior-create`, `etaf-define-behavior`, `etaf-current-behavior-context`, `etaf-focusable`, `etaf-toggleable` | Reuse non-visual interaction bundles | | Behaviors | `etaf-behavior-create`, `etaf-define-behavior`, `etaf-current-behavior-context`, `etaf-focusable`, `etaf-toggleable` | Reuse non-visual interaction bundles |
| Data | `etaf-data-source`, `etaf-data-controller`, `etaf-data-memory-source`, `etaf-data-*` | Query, paginate, mutate, select, and stop data | | Data | `etaf-data-source`, `etaf-data-controller`, `etaf-data-memory-source`, `etaf-data-*` | Query, paginate, mutate, select, and stop data |
@ -583,3 +595,9 @@ Most applications need only `etaf-view`, `etaf-mount`, `etaf-define-component`,
- Keep writes out of rendering; use an event, Action, watch callback, or Effect. - Keep writes out of rendering; use an event, Action, watch callback, or Effect.
- Use `etaf-ui` Components for product controls; core Hosts are the structural foundation. - Use `etaf-ui` Components for product controls; core Hosts are the structural foundation.
- Stop a Data Controller and unmount a Runtime when their owner is no longer needed. - Stop a Data Controller and unmount a Runtime when their owner is no longer needed.
For retained updates, keep the application pair declarative: `.etaf` contains
the static shell and the same-basename `.el` companion owns state, Components,
and actions. Reactive writes are batched into one generation publication;
failed publication is retryable, and a non-converging effect is reported rather
than allowed to keep the UI busy.

View File

@ -58,6 +58,8 @@ ETAF 依赖独立的 Ebox 包。开发时把核心检出目录放入 `load-path`
View 含有状态型 Component、响应式数据、事件或生命周期时使用 `etaf-mount` View 含有状态型 Component、响应式数据、事件或生命周期时使用 `etaf-mount`
当前 core 不直接加载 `.etaf`。`etaf-define-component` 是结构/样式/行为单元:用 View 定义结构,用 `:styles` 放静态 presentation`:setup` 管理 retained state、Action 和生命周期。未来 `.etaf` SFC 属于把结果编译成同一套 Component 契约的 compiler layer而不是第二个 Runtime 入口。
## 3. 属性与子节点 ## 3. 属性与子节点
属性值就是普通 Elisp 表达式,不需要额外的 `expr` 属性值就是普通 Elisp 表达式,不需要额外的 `expr`
@ -266,6 +268,13 @@ Setup 对 retained instance 只执行一次,返回的 render 函数在更新
显式 Host 属性 > Component :styles > 继承的 Theme 默认值 显式 Host 属性 > Component :styles > 继承的 Theme 默认值
``` ```
在同一个 Component 样式作用域内,第一条匹配声明会填充尚未指定的 Host
属性;后续规则不会覆盖已经解析出的默认值。非 nil 的 Host 属性始终受
保护nil Host 属性按省略处理,可以接收 Component 或 Theme 默认值。若
variant 需要明确覆盖,应使用显式 Host 属性或不同的属性键。
样式作用域归属于创建 View node 的 Component。父组件规则不会进入嵌套 Component 的内部;调用者通过 slot 提供的内容保留 caller scopeComponent 自己定义的 slot fallback 保留 child scope。
Theme 是 Context 的便捷形式,不是另一个 Runtime 对象: Theme 是 Context 的便捷形式,不是另一个 Runtime 对象:
```elisp ```elisp
@ -339,7 +348,7 @@ Behavior 用来打包可复用的非视觉属性和 cleanup
(message "Behavior removed"))))))) (message "Behavior removed")))))))
``` ```
Installer 需要 Runtime 或 Host path 时,可以调用 `etaf-current-behavior-context`。Behavior 被替换时,旧 cleanup 会在新状态成为当前状态前运行。 Installer 需要 Runtime 或 Host path 时,可以调用 `etaf-current-behavior-context`。Behavior 被替换时,旧 cleanup 会在新状态成为当前状态前运行。Behavior equality 对 function 和 reactive value 使用 `eq`;因此新建的 installer closure 会被视为有意替换,而不是错误复用。替换状态先以 mounted resource registry 的 staged resource 保存generation commit 后才成为 authority。
Focus 和 hit testing 是 Runtime 操作: Focus 和 hit testing 是 Runtime 操作:
@ -351,6 +360,8 @@ Focus 和 hit testing 是 Runtime 操作:
`etaf-host-ref-bounds``etaf-host-ref-position` 暴露公共的 Ebox hit-test 边界。`etaf-dispatch-event` 支持可选 payload 标记,用于给 callback 传入一个参数。 `etaf-host-ref-bounds``etaf-host-ref-position` 暴露公共的 Ebox hit-test 边界。`etaf-dispatch-event` 支持可选 payload 标记,用于给 callback 传入一个参数。
挂载后的 buffer 会自动启用 `etaf-input-mode``TAB` 聚焦下一个 Host`Shift-TAB`/backtab 聚焦上一个 Host`RET` 激活当前焦点 Host`mouse-1` 在点击位置激活 Host。焦点顺序先按数字 `:tab-index`,相同 index 再按 live buffer position 稳定排序;移动焦点也会移动 point。卸载时会禁用该输入 mode。
## 9. Context / Provide / Inject ## 9. Context / Provide / Inject
Context 适合跨多层共享依赖,不适合普通 label Context 适合跨多层共享依赖,不适合普通 label
@ -563,7 +574,7 @@ Loader 错误会保存在 `etaf-resource-error`cleanup/type 错误保持可
| 响应式 effect | `etaf-watch`、`etaf-watch-effect`、`etaf-effect-scope`、`etaf-scope-run` | 观察状态或同步外部资源 | | 响应式 effect | `etaf-watch`、`etaf-watch-effect`、`etaf-effect-scope`、`etaf-scope-run` | 观察状态或同步外部资源 |
| 生命周期 | `etaf-on-mounted`、`etaf-on-updated`、`etaf-on-unmounted` | 绑定 Component 生命周期工作 | | 生命周期 | `etaf-on-mounted`、`etaf-on-updated`、`etaf-on-unmounted` | 绑定 Component 生命周期工作 |
| Context | `etaf-provide`、`etaf-inject`、`etaf-theme-provide` | 跨层级共享依赖 | | Context | `etaf-provide`、`etaf-inject`、`etaf-theme-provide` | 跨层级共享依赖 |
| 事件与 focus | `etaf-dispatch-event`、`etaf-activate`、`etaf-focus`、`etaf-focus-next` | 进入交互 Runtime | | 事件与 focus | `etaf-dispatch-event`、`etaf-activate`、`etaf-focus`、`etaf-focus-next`、`etaf-focus-previous`、`etaf-input-mode` | 进入交互 Runtime |
| Action | `etaf-action-define`、`etaf-dispatch` | 命名和复用业务变更 | | Action | `etaf-action-define`、`etaf-dispatch` | 命名和复用业务变更 |
| Behavior | `etaf-behavior-create`、`etaf-define-behavior`、`etaf-current-behavior-context`、`etaf-focusable`、`etaf-toggleable` | 复用非视觉交互 bundle | | Behavior | `etaf-behavior-create`、`etaf-define-behavior`、`etaf-current-behavior-context`、`etaf-focusable`、`etaf-toggleable` | 复用非视觉交互 bundle |
| Data | `etaf-data-source`、`etaf-data-controller`、`etaf-data-memory-source`、`etaf-data-*` | 查询、分页、变更、选择和停止数据 | | Data | `etaf-data-source`、`etaf-data-controller`、`etaf-data-memory-source`、`etaf-data-*` | 查询、分页、变更、选择和停止数据 |
@ -583,3 +594,8 @@ Loader 错误会保存在 `etaf-resource-error`cleanup/type 错误保持可
- 不要在 render 中写状态使用事件、Action、watch callback 或 Effect。 - 不要在 render 中写状态使用事件、Action、watch callback 或 Effect。
- 产品级控件使用 `etaf-ui` Componentcore Host 只是结构基础。 - 产品级控件使用 `etaf-ui` Componentcore Host 只是结构基础。
- owner 不再需要时,停止 Data Controller 并卸载 Runtime。 - owner 不再需要时,停止 Data Controller 并卸载 Runtime。
对于保留式更新,建议保持 pair 结构:`.etaf` 只放静态 shell同名 `.el` companion
负责 state、Component 和 action。响应式写入会合并为一次 generation publication
发布失败可以在同一旧状态上重试non-converging effect 会报告错误,不会让界面
持续占用事件循环。

View File

@ -18,6 +18,13 @@
"Invalid ETAF Component definition" "Invalid ETAF Component definition"
'etaf-view-error) 'etaf-view-error)
(cl-defstruct (etaf--slot-content (:constructor etaf--slot-content-create))
"Internal caller ownership metadata for one slot entry."
owner-component-id children)
(defvar etaf--raw-slot-read-p nil
"Set while a Component render uses public raw slot accessors.")
(defun etaf--component-definition-error (format-string &rest arguments) (defun etaf--component-definition-error (format-string &rest arguments)
"Signal a Component definition error from FORMAT-STRING and ARGUMENTS." "Signal a Component definition error from FORMAT-STRING and ARGUMENTS."
(signal 'etaf-component-definition-error (signal 'etaf-component-definition-error
@ -63,21 +70,23 @@ The function is also useful to code that deliberately avoids that shorthand."
(defun etaf-current-slots () (defun etaf-current-slots ()
"Return the current Component's normalized slot alist." "Return the current Component's normalized slot alist."
etaf--current-component-slots) (setq etaf--raw-slot-read-p t)
(mapcar (lambda (entry)
(cons (car entry)
(if (etaf--slot-content-p (cdr entry))
(etaf--slot-content-children (cdr entry))
(cdr entry))))
etaf--current-component-slots))
(defun etaf-current-slot (name &optional fallback) (defun etaf-current-slot (name &optional fallback)
"Return the child list for slot NAME, or FALLBACK when it is absent." "Return the child list for slot NAME, or FALLBACK when it is absent."
(setq etaf--raw-slot-read-p t)
(let ((entry (assq name etaf--current-component-slots))) (let ((entry (assq name etaf--current-component-slots)))
(if entry (cdr entry) fallback))) (if entry
(if (etaf--slot-content-p (cdr entry))
(defun etaf--component-prop-bindings (props) (etaf--slot-content-children (cdr entry))
"Return lexical bindings for stateless Component PROPS." (cdr entry))
(mapcar (lambda (prop) fallback)))
`(,prop
(etaf--resolve-property-value
(plist-get etaf--component-props
,(etaf--component-prop-key prop)))))
props))
(defun etaf--component-prop-symbol-macros (props) (defun etaf--component-prop-symbol-macros (props)
"Return symbol macros that read current Component PROPS." "Return symbol macros that read current Component PROPS."
@ -183,7 +192,8 @@ forms do not use quote; ordinary Elisp belongs in `expr :value'."
(etaf--current-component-slots etaf--component-slots) (etaf--current-component-slots etaf--component-slots)
(etaf--current-component-instance (etaf--current-component-instance
etaf--current-component-instance)) etaf--current-component-instance))
(let ,(etaf--component-prop-bindings props) (cl-symbol-macrolet
,(etaf--component-prop-symbol-macros props)
,(etaf--compile-view-form view-form :projection)))))) ,(etaf--compile-view-form view-form :projection))))))
(setup-lambda (setup-lambda
(when saw-setup (when saw-setup

View File

@ -18,14 +18,28 @@
(:constructor etaf--context-create)) (:constructor etaf--context-create))
"One inherited Context frame owned by a Component instance." "One inherited Context frame owned by a Component instance."
parent parent
owner-id
(values (make-hash-table :test #'eq))) (values (make-hash-table :test #'eq)))
(defvar etaf--current-context nil (defvar etaf--current-context nil
"Context table of the Component currently being evaluated.") "Context table of the Component currently being evaluated.")
(defvar etaf--context-inject-recorder nil
"Candidate-local function recording Context provider reads.")
(defconst etaf--context-missing (make-symbol "etaf-context-missing") (defconst etaf--context-missing (make-symbol "etaf-context-missing")
"Unique sentinel used to distinguish an absent Context value.") "Unique sentinel used to distinguish an absent Context value.")
(defun etaf-context-copy (context)
"Return an immutable candidate copy of CONTEXT's local frame."
(when context
(let ((values (make-hash-table :test #'eq)))
(maphash (lambda (key value) (puthash key value values))
(etaf-context-values context))
(etaf--context-create :parent (etaf-context-parent context)
:owner-id (etaf-context-owner-id context)
:values values))))
(defun etaf--context-key (key) (defun etaf--context-key (key)
"Validate and normalize Context KEY." "Validate and normalize Context KEY."
(unless (and (symbolp key) (not (keywordp key)) (not (memq key '(nil t)))) (unless (and (symbolp key) (not (keywordp key)) (not (memq key '(nil t))))
@ -52,16 +66,21 @@ can be distinguished from a missing dependency."
(let ((key (etaf--context-key key)) (let ((key (etaf--context-key key))
(context etaf--current-context) (context etaf--current-context)
found found
provider
value) value)
(while (and context (not found)) (while (and context (not found))
(let ((candidate (gethash key (etaf-context-values context) (let ((candidate (gethash key (etaf-context-values context)
etaf--context-missing))) etaf--context-missing)))
(unless (eq candidate etaf--context-missing) (unless (eq candidate etaf--context-missing)
(setq value candidate (setq value candidate
found t))) found t
provider context)))
(setq context (etaf-context-parent context))) (setq context (etaf-context-parent context)))
(if found (if found
value (progn
(when etaf--context-inject-recorder
(funcall etaf--context-inject-recorder provider key))
value)
(if (and required-p-supplied-p required-p) (if (and required-p-supplied-p required-p)
(signal 'etaf-context-error (signal 'etaf-context-error
(list (format "Missing ETAF Context dependency: %S" key))) (list (format "Missing ETAF Context dependency: %S" key)))

View File

@ -4,9 +4,9 @@
;;; Commentary: ;;; Commentary:
;; Events are Runtime-owned entry points into local `:on-*' callbacks. Ebox ;; Events are Runtime-owned entry points into local `:on-*' callbacks. The
;; supplies live Host reference bounds; ETAF decides which callback to invoke ;; input minor mode is deliberately small: Runtime only toggles it at mount
;; and keeps focus state separate from the visual tree. ;; boundaries, while this module owns key and mouse command definitions.
;;; Code: ;;; Code:
@ -20,10 +20,14 @@
(declare-function etaf-runtime-p "etaf-runtime" (value)) (declare-function etaf-runtime-p "etaf-runtime" (value))
(declare-function etaf-runtime-mounted-p "etaf-runtime" (runtime)) (declare-function etaf-runtime-mounted-p "etaf-runtime" (runtime))
(declare-function etaf-runtime-buffer "etaf-runtime" (runtime)) (declare-function etaf-runtime-buffer "etaf-runtime" (runtime))
(declare-function etaf-runtime-handlers "etaf-runtime" (runtime)) (declare-function etaf-runtime-handler-for "etaf-runtime" (runtime host-ref))
(declare-function etaf-runtime-host-props "etaf-runtime" (runtime)) (declare-function etaf-runtime-handler-entries "etaf-runtime" (runtime))
(declare-function etaf-runtime-host-props-for "etaf-runtime" (runtime host-ref))
(declare-function etaf-runtime-host-props-entries "etaf-runtime" (runtime))
(declare-function etaf-runtime-focus-ref "etaf-runtime" (runtime)) (declare-function etaf-runtime-focus-ref "etaf-runtime" (runtime))
(declare-function etaf-runtime-set-focus-ref "etaf-runtime" (runtime host-ref)) (declare-function etaf-runtime-set-focus-ref "etaf-runtime" (runtime host-ref))
(declare-function etaf-runtime-event-begin "etaf-runtime" (runtime))
(declare-function etaf-runtime-event-end "etaf-runtime" (runtime))
(define-error 'etaf-event-error "Invalid ETAF Event") (define-error 'etaf-event-error "Invalid ETAF Event")
@ -46,7 +50,7 @@
(defun etaf--event-handler (runtime host-ref kind) (defun etaf--event-handler (runtime host-ref kind)
"Return callback for HOST-REF and KIND in RUNTIME, or nil." "Return callback for HOST-REF and KIND in RUNTIME, or nil."
(cdr (assq (etaf-event-kind kind) (cdr (assq (etaf-event-kind kind)
(gethash host-ref (etaf-runtime-handlers runtime))))) (etaf-runtime-handler-for runtime host-ref))))
;;;###autoload ;;;###autoload
(cl-defun etaf-dispatch-event (cl-defun etaf-dispatch-event
@ -65,9 +69,12 @@ otherwise call the local callback with no arguments."
(etaf--current-event (etaf--current-event
(list :runtime runtime :host-ref host-ref (list :runtime runtime :host-ref host-ref
:kind (etaf-event-kind kind) :payload payload))) :kind (etaf-event-kind kind) :payload payload)))
(etaf-runtime-event-begin runtime)
(unwind-protect
(if payload-p (if payload-p
(funcall callback payload) (funcall callback payload)
(funcall callback))))) (funcall callback))
(etaf-runtime-event-end runtime)))))
;;;###autoload ;;;###autoload
(defun etaf-host-ref-bounds (runtime host-ref) (defun etaf-host-ref-bounds (runtime host-ref)
@ -81,71 +88,184 @@ otherwise call the local callback with no arguments."
(setq runtime (etaf-runtime-require-mounted runtime)) (setq runtime (etaf-runtime-require-mounted runtime))
(ebox-host-ref-position (etaf-runtime-buffer runtime) host-ref)) (ebox-host-ref-position (etaf-runtime-buffer runtime) host-ref))
(defun etaf--position-number (position)
"Return integer POSITION, or nil when POSITION is not buffer-local."
(cond
((markerp position) (marker-position position))
((integerp position) position)
(t nil)))
(defun etaf--activation-candidate-before-p (left right)
"Return non-nil when activation candidate LEFT precedes RIGHT."
(let ((left-length (nth 2 left))
(right-length (nth 2 right))
(left-start (nth 1 left))
(right-start (nth 1 right)))
(or (< left-length right-length)
(and (= left-length right-length)
(or (< left-start right-start)
(and (= left-start right-start)
(string< (prin1-to-string (car left))
(prin1-to-string (car right)))))))))
(defun etaf--activation-at-position (runtime position &optional quiet)
"Activate the smallest enabled Host at POSITION in RUNTIME.
When QUIET is non-nil, return nil instead of signaling when no callback owns
the position."
(let (candidates)
(dolist (entry (etaf-runtime-handler-entries runtime))
(let ((host-ref (car entry)) (handlers (cdr entry)))
(let* ((press (assq 'press handlers))
(props (etaf-runtime-host-props-for runtime host-ref))
(bounds (ebox-host-ref-bounds
(etaf-runtime-buffer runtime) host-ref))
(start (and bounds (car bounds)))
(end (and bounds (cdr bounds))))
(when (and press (not (plist-get props :disabled))
start end (<= start position) (< position end))
(push (list host-ref start (- end start)) candidates)))))
(setq candidates (sort candidates #'etaf--activation-candidate-before-p))
(if-let ((candidate (car candidates)))
(etaf-dispatch-event runtime (car candidate) 'press)
(unless quiet
(user-error "No interactive ETAF Host at point")))))
;;;###autoload ;;;###autoload
(defun etaf-activate (&optional runtime) (defun etaf-activate (&optional runtime)
"Dispatch `press' for the smallest active Host containing point. "Dispatch `press' for the smallest enabled Host containing point.
RUNTIME is the mounted Runtime to activate, or nil for the current Runtime. RUNTIME is the mounted Runtime to activate, or nil for the current buffer."
Return the callback result. This command is intentionally a thin adapter; (interactive)
hit testing remains Ebox's public Host-reference bounds operation."
(setq runtime (etaf-runtime-require-mounted runtime)) (setq runtime (etaf-runtime-require-mounted runtime))
(let ((position (point)) candidates) (let ((position (with-current-buffer (etaf-runtime-buffer runtime)
(maphash (point))))
(lambda (host-ref _handlers) (etaf--activation-at-position runtime position)))
(when-let ((bounds (ebox-host-ref-bounds
(defun etaf--focus-candidate-before-p (left right)
"Return non-nil when focus candidate LEFT precedes RIGHT."
(or (< (nth 0 left) (nth 0 right))
(and (= (nth 0 left) (nth 0 right))
(or (< (nth 1 left) (nth 1 right))
(and (= (nth 1 left) (nth 1 right))
(or (< (nth 2 left) (nth 2 right))
(and (= (nth 2 left) (nth 2 right))
(string< (prin1-to-string (nth 3 left))
(prin1-to-string (nth 3 right))))))))))
(defun etaf--focus-candidates (runtime)
"Return RUNTIME's visible focus candidates in stable buffer order."
(let (candidates)
(dolist (entry (etaf-runtime-host-props-entries runtime))
(let ((host-ref (car entry)) (props (cdr entry)))
(let ((tab-index (plist-get props :tab-index))
(bounds (ebox-host-ref-bounds
(etaf-runtime-buffer runtime) host-ref))) (etaf-runtime-buffer runtime) host-ref)))
(when (and (<= (car bounds) position) (< position (cdr bounds))) (when (and (numberp tab-index) (>= tab-index 0)
(push (cons host-ref (- (cdr bounds) (car bounds))) candidates)))) (not (plist-get props :disabled)) bounds)
(etaf-runtime-handlers runtime)) (push (list tab-index (car bounds) (cdr bounds) host-ref)
(unless candidates candidates)))))
(user-error "No interactive ETAF Host at point")) (sort candidates #'etaf--focus-candidate-before-p)))
(let* ((ordered (sort candidates
(lambda (left right) (< (cdr left) (cdr right)))))
(host-ref (car (car ordered))))
(etaf-dispatch-event runtime host-ref 'press))))
;;;###autoload ;;;###autoload
(defun etaf-focus (runtime host-ref) (defun etaf-focus (&optional runtime host-ref)
"Move focus to HOST-REF in mounted RUNTIME and return HOST-REF." "Move focus to HOST-REF in mounted RUNTIME and move point to its position.
When called interactively without arguments, focus the first visible Host."
(interactive)
(setq runtime (etaf-runtime-require-mounted runtime)) (setq runtime (etaf-runtime-require-mounted runtime))
(unless (etaf-host-ref-bounds runtime host-ref) (unless host-ref
(setq host-ref (nth 3 (car (etaf--focus-candidates runtime))))
(unless host-ref
(user-error "No focusable ETAF Host")))
(let ((position (etaf-host-ref-position runtime host-ref)))
(unless position
(signal 'etaf-event-error (signal 'etaf-event-error
(list (format "Cannot focus an invisible Host reference: %S" (list (format "Cannot focus an invisible Host reference: %S"
host-ref)))) host-ref))))
(etaf-runtime-set-focus-ref runtime host-ref)) (etaf-runtime-set-focus-ref runtime host-ref)
(with-current-buffer (etaf-runtime-buffer runtime)
(goto-char position))
host-ref))
;;;###autoload ;;;###autoload
(defun etaf-focused-host-ref (&optional runtime) (defun etaf-focused-host-ref (&optional runtime)
"Return the currently focused Host reference in mounted RUNTIME." "Return the currently focused Host reference in mounted RUNTIME."
(etaf-runtime-focus-ref (etaf-runtime-require-mounted runtime))) (etaf-runtime-focus-ref (etaf-runtime-require-mounted runtime)))
(defun etaf--focus-cycle (runtime step)
"Move focus by STEP positions through RUNTIME's ordered candidates."
(setq runtime (etaf-runtime-require-mounted runtime))
(let* ((candidates (etaf--focus-candidates runtime))
(ordered (mapcar (lambda (entry) (nth 3 entry)) candidates))
(current (etaf-runtime-focus-ref runtime))
(position (cl-position current ordered :test #'equal)))
(unless ordered
(user-error "No focusable ETAF Host"))
(etaf-focus runtime
(if position
(nth (mod (+ position step) (length ordered)) ordered)
(if (> step 0) (car ordered) (car (last ordered)))))))
;;;###autoload ;;;###autoload
(defun etaf-focus-next (&optional runtime) (defun etaf-focus-next (&optional runtime)
"Focus the next visible Host in mounted RUNTIME. "Focus the next visible Host in mounted RUNTIME."
(interactive)
(etaf--focus-cycle runtime 1))
RUNTIME may be nil to use the current Runtime. Hosts need a numeric, ;;;###autoload
non-negative `:tab-index'." (defun etaf-focus-previous (&optional runtime)
(setq runtime (etaf-runtime-require-mounted runtime)) "Focus the previous visible Host in mounted RUNTIME."
(let (candidates) (interactive)
(maphash (etaf--focus-cycle runtime -1))
(lambda (host-ref props)
(let ((tab-index (plist-get props :tab-index))) (defun etaf-activate-mouse (event)
(when (and (numberp tab-index) (>= tab-index 0) "Activate the enabled Host at the real mouse EVENT position."
(etaf-host-ref-bounds runtime host-ref)) (interactive "e")
(push (cons tab-index host-ref) candidates)))) (let* ((start (event-start event))
(etaf-runtime-host-props runtime)) (window (posn-window start))
(setq candidates (sort candidates (position (etaf--position-number (posn-point start))))
(lambda (left right) (when (and (windowp window) position)
(< (car left) (car right))))) (let ((buffer (window-buffer window)))
(unless candidates (when (buffer-live-p buffer)
(user-error "No focusable ETAF Host")) (with-current-buffer buffer
(let* ((current (etaf-runtime-focus-ref runtime)) (when-let ((runtime (etaf-runtime-for-buffer buffer)))
(ordered (mapcar #'cdr candidates)) (goto-char position)
(position (cl-position current ordered :test #'equal)) (etaf--activation-at-position runtime position t))))))))
(next (if position
(or (nth (1+ position) ordered) (car ordered)) (defvar etaf-input-mode-map
(car ordered)))) (let ((map (make-sparse-keymap)))
(etaf-focus runtime next)))) (define-key map (kbd "TAB") #'etaf-focus-next)
(define-key map [tab] #'etaf-focus-next)
(define-key map (kbd "<backtab>") #'etaf-focus-previous)
(define-key map [backtab] #'etaf-focus-previous)
(define-key map (kbd "S-TAB") #'etaf-focus-previous)
(define-key map (kbd "RET") #'etaf-activate)
(define-key map (kbd "<return>") #'etaf-activate)
(define-key map [mouse-1] #'etaf-activate-mouse)
map)
"Keymap for mounted ETAF Runtime input.")
;;;###autoload
(define-minor-mode etaf-input-mode
"Enable keyboard and mouse input for a mounted ETAF Runtime."
:init-value nil
:lighter " ETAF"
:keymap etaf-input-mode-map)
;;;###autoload
(defun etaf-events-enable-input (buffer)
"Enable `etaf-input-mode' in mounted BUFFER."
(with-current-buffer buffer
(etaf-input-mode 1)))
;;;###autoload
(defun etaf-events-disable-input (buffer)
"Disable `etaf-input-mode' in BUFFER after Runtime unmount."
(when (buffer-live-p buffer)
(with-current-buffer buffer
(when (bound-and-true-p etaf-input-mode)
(etaf-input-mode -1)))))
(provide 'etaf-events) (provide 'etaf-events)

View File

@ -25,6 +25,7 @@
test test
(version 0) (version 0)
subscribers subscribers
source-id
name) name)
(cl-defstruct (etaf-computed (cl-defstruct (etaf-computed
@ -35,6 +36,7 @@
test test
(version 0) (version 0)
subscribers subscribers
source-id
effect effect
(dirty-p t) (dirty-p t)
evaluated-p evaluated-p
@ -63,15 +65,31 @@
name name
(active-p t)) (active-p t))
(cl-defstruct (etaf-runtime-route
(:constructor etaf-runtime-route-create))
"Opaque Runtime route stored by reactive sources."
runtime-id mount-epoch scheduler)
(defvar etaf--active-effect nil (defvar etaf--active-effect nil
"The effect currently collecting dependencies.") "The effect currently collecting dependencies.")
(defvar etaf--source-id-counter 0)
(defun etaf-reactive-source-id (source)
"Return stable integer id of reactive SOURCE."
(cond ((etaf-ref-p source) (etaf-ref-source-id source))
((etaf-computed-p source) (etaf-computed-source-id source))
(t (signal 'wrong-type-argument (list 'etaf-reactive-source-p source)))))
(defvar etaf--active-scope nil (defvar etaf--active-scope nil
"The Scope that owns newly created effects and cleanups.") "The Scope that owns newly created effects and cleanups.")
(defvar etaf--tracking-enabled-p t (defvar etaf--tracking-enabled-p t
"Whether reactive reads may collect dependencies.") "Whether reactive reads may collect dependencies.")
(defvar etaf--runtime-dependency-collector nil
"Runtime-local collector called for each reactive source read.")
(defvar etaf--render-phase-p nil (defvar etaf--render-phase-p nil
"Whether the current call is producing a View tree.") "Whether the current call is producing a View tree.")
@ -81,6 +99,53 @@
The function receives a zero-argument job and a phase symbol. Outside a The function receives a zero-argument job and a phase symbol. Outside a
Runtime, watchers run synchronously.") Runtime, watchers run synchronously.")
(defvar etaf--dispatch-depth 0)
(defvar etaf--dispatch-source-queue nil)
(defvar etaf--dispatch-source-set (make-hash-table :test #'eq))
(defvar etaf--dispatch-runtime-queue nil)
(defvar etaf--dispatch-runtime-set (make-hash-table :test #'eq))
(defvar etaf--dispatch-effect-set (make-hash-table :test #'eq))
(defun etaf-reactive-enqueue-runtime-flush (runtime function)
"Queue FUNCTION once for RUNTIME after the outer reactive dispatch settles."
(unless (gethash runtime etaf--dispatch-runtime-set)
(puthash runtime function etaf--dispatch-runtime-set)
(setq etaf--dispatch-runtime-queue
(nconc etaf--dispatch-runtime-queue (list runtime)))))
(defun etaf--dispatch-source-now (source)
"Notify SOURCE subscribers without opening another dispatch boundary."
(let ((subscribers (copy-hash-table (etaf--source-subscribers source))))
(maphash
(lambda (subscriber _)
(cond
((etaf-runtime-route-p subscriber)
(funcall (etaf-runtime-route-scheduler subscriber)
subscriber source))
((etaf-effect-active-p subscriber)
(unless (gethash subscriber etaf--dispatch-effect-set)
(puthash subscriber t etaf--dispatch-effect-set)
(if-let ((scheduler (etaf-effect-scheduler subscriber)))
(funcall scheduler subscriber)
(etaf-reactive-effect-run subscriber))))))
subscribers)))
(defun etaf--drain-dispatch ()
"Drain reactive sources and Runtime work to a stable outer fixed point."
(while (or etaf--dispatch-source-queue etaf--dispatch-runtime-queue)
(while etaf--dispatch-source-queue
(let ((source (pop etaf--dispatch-source-queue)))
(remhash source etaf--dispatch-source-set)
(etaf--dispatch-source-now source)))
;; Detach this turn. A lifecycle write may enqueue a source and the same
;; Runtime again for the following turn without merging it into this one.
(let ((turn etaf--dispatch-runtime-queue))
(setq etaf--dispatch-runtime-queue nil)
(dolist (runtime turn)
(let ((function (gethash runtime etaf--dispatch-runtime-set)))
(remhash runtime etaf--dispatch-runtime-set)
(funcall function))))))
(defun etaf--reactive-same-p (left right) (defun etaf--reactive-same-p (left right)
"Return whether LEFT and RIGHT are equal under ETAF's shallow rule." "Return whether LEFT and RIGHT are equal under ETAF's shallow rule."
(if (or (consp left) (vectorp left) (hash-table-p left) (if (or (consp left) (vectorp left) (hash-table-p left)
@ -104,6 +169,9 @@ Runtime, watchers run synchronously.")
(defun etaf--track-source (source) (defun etaf--track-source (source)
"Record SOURCE as a dependency of the active effect." "Record SOURCE as a dependency of the active effect."
(when (and etaf--tracking-enabled-p etaf--runtime-dependency-collector
(null etaf--active-effect))
(funcall etaf--runtime-dependency-collector source))
(when (and etaf--tracking-enabled-p etaf--active-effect (when (and etaf--tracking-enabled-p etaf--active-effect
(etaf-effect-active-p etaf--active-effect)) (etaf-effect-active-p etaf--active-effect))
(let ((subscribers (etaf--source-subscribers source))) (let ((subscribers (etaf--source-subscribers source)))
@ -169,6 +237,7 @@ of the run."
(etaf--clear-effect-deps effect) (etaf--clear-effect-deps effect)
(let ((etaf--active-effect effect) (let ((etaf--active-effect effect)
(etaf--active-scope (etaf-effect-owner-scope effect)) (etaf--active-scope (etaf-effect-owner-scope effect))
(etaf--runtime-dependency-collector nil)
(etaf--tracking-enabled-p t) (etaf--tracking-enabled-p t)
(etaf--render-phase-p (or rendering etaf--render-phase-p))) (etaf--render-phase-p (or rendering etaf--render-phase-p)))
(setf (etaf-effect-running-p effect) t) (setf (etaf-effect-running-p effect) t)
@ -186,14 +255,18 @@ of the run."
(defun etaf--dispatch-source (source) (defun etaf--dispatch-source (source)
"Notify every current subscriber of SOURCE once." "Notify every current subscriber of SOURCE once."
(let ((subscribers (copy-hash-table (etaf--source-subscribers source)))) (unless (gethash source etaf--dispatch-source-set)
(maphash (puthash source t etaf--dispatch-source-set)
(lambda (effect _) (setq etaf--dispatch-source-queue
(when (etaf-effect-active-p effect) (nconc etaf--dispatch-source-queue (list source))))
(if-let ((scheduler (etaf-effect-scheduler effect))) (when (zerop etaf--dispatch-depth)
(funcall scheduler effect) (let ((etaf--dispatch-depth 1))
(etaf-reactive-effect-run effect)))) (unwind-protect (etaf--drain-dispatch)
subscribers))) (setq etaf--dispatch-source-queue nil
etaf--dispatch-runtime-queue nil)
(clrhash etaf--dispatch-source-set)
(clrhash etaf--dispatch-runtime-set)
(clrhash etaf--dispatch-effect-set)))))
;;;###autoload ;;;###autoload
(cl-defun etaf-ref (initial-value &key test name) (cl-defun etaf-ref (initial-value &key test name)
@ -205,6 +278,7 @@ TEST optionally compares old and new values. NAME is used in diagnostics."
(etaf--ref-create :value initial-value (etaf--ref-create :value initial-value
:test test :test test
:subscribers (make-hash-table :test #'eq) :subscribers (make-hash-table :test #'eq)
:source-id (cl-incf etaf--source-id-counter)
:name name)) :name name))
(defun etaf--computed-value (computed) (defun etaf--computed-value (computed)
@ -241,7 +315,8 @@ computed value."
(signal 'wrong-type-argument (list 'functionp test))) (signal 'wrong-type-argument (list 'functionp test)))
(let ((computed (etaf--computed-create (let ((computed (etaf--computed-create
:getter getter :test test :name name :getter getter :test test :name name
:subscribers (make-hash-table :test #'eq)))) :subscribers (make-hash-table :test #'eq)
:source-id (cl-incf etaf--source-id-counter))))
(setf (etaf-computed-effect computed) (setf (etaf-computed-effect computed)
(etaf-reactive-effect-create (etaf-reactive-effect-create
getter getter

View File

@ -14,9 +14,23 @@
(require 'cl-lib) (require 'cl-lib)
(require 'ebox) (require 'ebox)
(require 'etaf-view) (require 'etaf-view)
(require 'etaf-component)
(require 'etaf-context) (require 'etaf-context)
(declare-function etaf--runtime-render-component "etaf-runtime" (runtime call path)) (declare-function etaf--runtime-render-component "etaf-runtime" (runtime call path))
(defvar etaf--rendered-range-container-nodes nil
"Candidate-local Ebox nodes whose layout directly contains a Range.")
(declare-function etaf--runtime-render-child-range "etaf-runtime" (runtime expr path))
(declare-function etaf--runtime-render-slot-range
"etaf-runtime" (runtime projection path))
(declare-function etaf--runtime-register-semantic-host "etaf-runtime" (runtime name props backend-props path))
(declare-function etaf--runtime-finish-semantic-host
"etaf-runtime" (runtime semantic-id &optional content content-parts))
(declare-function etaf--runtime-render-inline-content
"etaf-runtime" (runtime host-id values path &optional surface))
(declare-function etaf--runtime-call-with-component-env
"etaf-runtime" (runtime component-id function))
(declare-function etaf--runtime-behavior-node "etaf-runtime" (runtime node path)) (declare-function etaf--runtime-behavior-node "etaf-runtime" (runtime node path))
(declare-function etaf--runtime-register-host "etaf-runtime" (runtime props path)) (declare-function etaf--runtime-register-host "etaf-runtime" (runtime props path))
(declare-function etaf-theme-defaults "etaf-context" (&optional default)) (declare-function etaf-theme-defaults "etaf-context" (&optional default))
@ -35,6 +49,15 @@
(defvar etaf--render-style-stack nil (defvar etaf--render-style-stack nil
"Component style scopes active during View lowering.") "Component style scopes active during View lowering.")
(defvar etaf--render-parent-style-stack nil
"Style scopes belonging to the caller of the current Component.")
(defvar etaf--current-semantic-parent-id 0
"Semantic parent receiving Hosts during mounted lowering.")
(defvar etaf--rendering-range-p nil
"Non-nil while eagerly lowering descendants of one Range item Host.")
(defun etaf--event-property-p (property) (defun etaf--event-property-p (property)
"Return non-nil when PROPERTY is an ETAF event callback property." "Return non-nil when PROPERTY is an ETAF event callback property."
(and (keywordp property) (and (keywordp property)
@ -94,7 +117,9 @@ represented as ordinary static data until this final lowering step; they do
not create another visual node category." not create another visual node category."
(if (null styles) (if (null styles)
node node
(let ((props (copy-sequence (etaf--view-node-props node)))) (let ((props (copy-sequence
(etaf--resolve-property-plist
(etaf--view-node-props node)))))
(dolist (rule (cdr styles)) (dolist (rule (cdr styles))
(when (and (consp rule) (stringp (car rule)) (when (and (consp rule) (stringp (car rule))
(etaf--style-selector-match-p (car rule) node root-p)) (etaf--style-selector-match-p (car rule) node root-p))
@ -109,29 +134,44 @@ not create another visual node category."
(setq value (if (and (consp value) (eq (car value) 'quote)) (setq value (if (and (consp value) (eq (car value) 'quote))
(cadr value) (cadr value)
value)) value))
(unless (plist-member props key) (unless (plist-get props key)
(setq props (append props (list key value))))))))) (setq props (etaf--merge-property props key value))))))))
(etaf--view-node-create (etaf--view-node-create
:name (etaf--view-node-name node) :name (etaf--view-node-name node)
:token (etaf--view-node-token node)
:props props :props props
:children (etaf--view-node-children node))))) :children (etaf--view-node-children node)))))
(defun etaf--apply-theme-defaults (node) (defun etaf--apply-theme-defaults (node)
"Apply inherited Theme defaults to NODE without overriding props." "Apply known inherited Theme defaults to NODE without overriding props.
Unknown Theme keys remain readable through `etaf-theme-value' but are design
tokens, not Ebox Host properties, and therefore are not materialized here."
(let ((defaults (etaf-theme-defaults))) (let ((defaults (etaf-theme-defaults)))
(if (null defaults) (if (null defaults)
node node
(let ((props (copy-sequence (etaf--view-node-props node)))) (let ((props (copy-sequence
(etaf--resolve-property-plist
(etaf--view-node-props node)))))
(while defaults (while defaults
(let ((key (pop defaults)) (let ((key (pop defaults))
(value (pop defaults))) (value (pop defaults)))
(unless (plist-member props key) (when (and (etaf--theme-ebox-property-p key)
(setq props (append props (list key value)))))) (not (plist-get props key)))
(setq props (etaf--merge-property props key value)))))
(etaf--view-node-create (etaf--view-node-create
:name (etaf--view-node-name node) :name (etaf--view-node-name node)
:token (etaf--view-node-token node)
:props props :props props
:children (etaf--view-node-children node)))))) :children (etaf--view-node-children node))))))
(defun etaf--theme-ebox-property-p (key)
"Return non-nil when Theme KEY is a known Ebox property.
The public `ebox-property-rules' table is the shared property vocabulary;
custom Theme tokens remain Context data and are never copied onto Hosts."
(and (keywordp key)
(boundp 'ebox-property-rules)
(assq key ebox-property-rules)))
(defun etaf--inline-text-surface-properties (props) (defun etaf--inline-text-surface-properties (props)
"Return text properties represented by inline text PROPS." "Return text properties represented by inline text PROPS."
(let ((surface (copy-sequence (or (plist-get props :surface-properties) nil)))) (let ((surface (copy-sequence (or (plist-get props :surface-properties) nil))))
@ -145,6 +185,13 @@ not create another visual node category."
(list 'background (plist-get props :bgcolor))))) (list 'background (plist-get props :bgcolor)))))
surface)) surface))
(defun etaf--apply-inline-surface-properties (content surface)
"Return CONTENT with resolved inline SURFACE properties applied."
(let ((result (copy-sequence content)))
(when (and surface (not (zerop (length result))))
(add-text-properties 0 (length result) surface result))
result))
(defun etaf--inline-text-content (value) (defun etaf--inline-text-content (value)
"Return `(SUCCESS . STRING)' for inline-compatible text VALUE. "Return `(SUCCESS . STRING)' for inline-compatible text VALUE.
@ -186,6 +233,18 @@ the ordinary layout lowering path instead."
(cons valid-p (and valid-p result)))) (cons valid-p (and valid-p result))))
(t (cons nil nil)))) (t (cons nil nil))))
(defun etaf--inline-text-structural-p (value)
"Return non-nil when VALUE can be owned by mounted inline effects."
(cond
((or (null value) (stringp value) (etaf--expr-p value)) t)
((etaf--view-node-p value)
(and (eq (etaf--view-node-name value) 'text)
(cl-every #'etaf--inline-text-structural-p
(etaf--view-node-children value))))
((proper-list-p value)
(cl-every #'etaf--inline-text-structural-p value))
(t nil)))
(defun etaf--ebox-properties (props path) (defun etaf--ebox-properties (props path)
"Translate ETAF PROPS at PATH into Ebox's public property list." "Translate ETAF PROPS at PATH into Ebox's public property list."
(let* ((source-props (etaf--resolve-property-plist props)) (let* ((source-props (etaf--resolve-property-plist props))
@ -237,13 +296,50 @@ the ordinary layout lowering path instead."
(defun etaf--render-slot-projection (projection path) (defun etaf--render-slot-projection (projection path)
"Render PROJECTION at PATH using the active Component slot collection." "Render PROJECTION at PATH using the active Component slot collection."
(setq etaf--raw-slot-read-p t)
(let* ((name (etaf--slot-projection-name projection)) (let* ((name (etaf--slot-projection-name projection))
(slots etaf--current-component-slots) (slots etaf--current-component-slots)
(entry (assq name slots)) (entry (assq name slots))
(content (and entry (cdr entry)))
(children (if (etaf--slot-content-p content)
(etaf--slot-content-children content)
content))
(owner-id (and (etaf--slot-content-p content)
(etaf--slot-content-owner-component-id content)))
(fallback (etaf--slot-projection-fallback projection))) (fallback (etaf--slot-projection-fallback projection)))
(let ((etaf--render-style-stack
(if entry
etaf--render-parent-style-stack
etaf--render-style-stack)))
(let ((render (lambda ()
(etaf--render-value-list (etaf--render-value-list
(if entry (cdr entry) fallback) (if entry children fallback)
(append path (list :slot name))))) (append path (list :slot name))))))
(if (and entry etaf--render-runtime)
(etaf--runtime-call-with-component-env
etaf--render-runtime owner-id render)
(funcall render))))))
(defun etaf--slot-projection-range-compatible-p (projection)
"Return whether mounted PROJECTION can use the current slot Range slice."
(let* ((entry (assq (etaf--slot-projection-name projection)
etaf--current-component-slots))
(content (and entry (cdr entry)))
(values (if entry
(if (etaf--slot-content-p content)
(etaf--slot-content-children content)
content)
(etaf--slot-projection-fallback projection))))
(cl-labels
((valid
(value)
(cond ((or (null value) (stringp value) (etaf--expr-p value)) t)
((etaf--view-node-p value)
(and (not (eq (etaf--view-node-name value) 'fragment))
(cl-every #'valid (etaf--view-node-children value))))
((proper-list-p value) (cl-every #'valid value))
(t nil))))
(valid values))))
(defun etaf--render-component-call-pure (call path) (defun etaf--render-component-call-pure (call path)
"Render stateless CALL at PATH without a retained Runtime." "Render stateless CALL at PATH without a retained Runtime."
@ -256,11 +352,11 @@ the ordinary layout lowering path instead."
(etaf--component-call-props call)) (etaf--component-call-props call))
(etaf--current-component-slots (etaf--current-component-slots
(etaf--component-call-slots call))) (etaf--component-call-slots call)))
(let ((etaf--render-style-stack (let ((etaf--render-parent-style-stack etaf--render-style-stack)
(cons (cons (etaf--component-spec-styles spec) (etaf--render-style-stack
(list (cons (etaf--component-spec-styles spec)
(append path (list :component (append path (list :component
(etaf--component-spec-name spec)))) (etaf--component-spec-name spec)))))))
etaf--render-style-stack)))
(etaf--render-value-list (etaf--render-value-list
(funcall render (funcall render
(etaf--component-call-props call) (etaf--component-call-props call)
@ -278,13 +374,24 @@ the ordinary layout lowering path instead."
path) path)
append append
(cond (cond
((stringp item) (list (ebox-create :content item))) ((stringp item)
(when etaf--render-runtime
(let ((semantic-id
(etaf--runtime-register-semantic-host
etaf--render-runtime 'text nil nil item-path)))
(etaf--runtime-finish-semantic-host
etaf--render-runtime semantic-id item)))
(list (ebox-create :content item)))
((etaf--view-node-p item) ((etaf--view-node-p item)
(etaf--render-node item item-path)) (etaf--render-node item item-path))
((etaf--component-call-p item) ((etaf--component-call-p item)
(if etaf--render-runtime (if etaf--render-runtime
(list (etaf--runtime-render-component (let ((result (etaf--runtime-render-component
etaf--render-runtime item item-path)) etaf--render-runtime item item-path)))
(if (and (consp result)
(eq (car result) 'component-output-range))
(cdr result)
(list result)))
(etaf--render-component-call-pure item item-path))) (etaf--render-component-call-pure item item-path)))
((etaf--slot-projection-p item) ((etaf--slot-projection-p item)
(etaf--render-slot-projection item item-path)) (etaf--render-slot-projection item item-path))
@ -328,40 +435,114 @@ the ordinary layout lowering path instead."
(etaf--runtime-register-host etaf--render-runtime raw-props path)) (etaf--runtime-register-host etaf--render-runtime raw-props path))
(pcase name (pcase name
('text ('text
(let* ((flat (cl-mapcan #'etaf--flatten-view-value children)) (let* ((semantic-id
(and etaf--render-runtime
(etaf--runtime-register-semantic-host
etaf--render-runtime name raw-props props path)))
(child-path (append path (list :text))) (child-path (append path (list :text)))
(inline (etaf--inline-text-content flat)) (runtime-inline
(and semantic-id
(etaf--inline-text-structural-p children)
(etaf--runtime-render-inline-content
etaf--render-runtime semantic-id children child-path)))
(flat (unless runtime-inline
(cl-mapcan #'etaf--flatten-view-value children)))
(inline (if runtime-inline
(cons t (car runtime-inline))
(etaf--inline-text-content flat)))
(rendered (unless (car inline) (rendered (unless (car inline)
(etaf--render-value-list flat child-path)))) (let ((etaf--current-semantic-parent-id
(or semantic-id
etaf--current-semantic-parent-id)))
(etaf--render-value-list flat child-path)))))
(when semantic-id
(etaf--runtime-finish-semantic-host
etaf--render-runtime semantic-id
(and (car inline) (cdr inline))
(and runtime-inline (cadr runtime-inline))))
(if (car inline) (if (car inline)
(list (apply #'ebox-create (list (apply #'ebox-create :content (cdr inline) props))
:content (cdr inline) (list (ebox-build (append (list 'row) props rendered))))))
props))
(list (ebox-build
(append (list 'row) props rendered))))))
('spacer ('spacer
(when children (when children
(signal 'etaf-renderer-error (signal 'etaf-renderer-error
(list "spacer cannot have children"))) (list "spacer cannot have children")))
(when etaf--render-runtime
(let ((semantic-id (etaf--runtime-register-semantic-host
etaf--render-runtime name raw-props props path)))
(etaf--runtime-finish-semantic-host
etaf--render-runtime semantic-id)))
(list (apply #'ebox-spacer props))) (list (apply #'ebox-spacer props)))
('fragment ('fragment
(etaf--render-value-list children (append path (list :fragment)))) (etaf--render-value-list children (append path (list :fragment))))
((or 'row 'column 'container 'stack 'flex 'grid) ((or 'row 'column 'container 'stack 'flex 'grid)
(let ((nodes nil) (let ((nodes nil)
(index 0)) (index 0)
(range-child-p nil)
(semantic-id
(and etaf--render-runtime
(etaf--runtime-register-semantic-host
etaf--render-runtime name raw-props props path))))
(dolist (child children) (dolist (child children)
(setq nodes (setq nodes
(nconc nodes (nconc nodes
(let ((etaf--current-semantic-parent-id
(or semantic-id
etaf--current-semantic-parent-id)))
(if (and semantic-id (etaf--expr-p child)
(not etaf--rendering-range-p))
(let ((result
(etaf--runtime-render-child-range
etaf--render-runtime child
(append path (list index)))))
(when (eq (car result) 'range)
(setq range-child-p t))
(cdr result))
(if (and semantic-id
(etaf--view-node-p child)
(eq (etaf--view-node-name child) 'fragment)
(not etaf--rendering-range-p))
(let ((result
(etaf--runtime-render-fragment-range
etaf--render-runtime child
(append path (list index)))))
(setq range-child-p t)
(cdr result))
(if (and semantic-id
(etaf--raw-ebox-p child)
(not etaf--rendering-range-p))
(let ((result
(etaf--runtime-render-raw-range
etaf--render-runtime child
(append path (list index)))))
(setq range-child-p t)
(cdr result))
(if (and semantic-id
(etaf--slot-projection-p child)
(etaf--slot-projection-range-compatible-p
child)
(not etaf--rendering-range-p))
(let ((result
(etaf--runtime-render-slot-range
etaf--render-runtime child
(append path (list index)))))
(setq range-child-p t)
(cdr result))
(etaf--render-value-list (etaf--render-value-list
child (append path (list index))))) child (append path (list index))))))))))
(cl-incf index)) (cl-incf index))
(list (etaf--layout-node name props nodes)))) (when semantic-id
(etaf--runtime-finish-semantic-host
etaf--render-runtime semantic-id))
(list (etaf--layout-node name props nodes range-child-p))))
(_ (_
(signal 'etaf-renderer-error (signal 'etaf-renderer-error
(list (format "Unknown Host reached renderer: %S" name))))))) (list (format "Unknown Host reached renderer: %S" name)))))))
(defun etaf--layout-node (name props nodes) (defun etaf--layout-node (name props nodes &optional range-child-p)
"Build layout NAME with Ebox PROPS around child NODES." "Build layout NAME with Ebox PROPS around child NODES.
RANGE-CHILD-P records that NODES include nonvisual Range segments."
(let ((node
(cond (cond
((eq name 'row) ((eq name 'row)
(if (null props) (if (null props)
@ -377,7 +558,26 @@ the ordinary layout lowering path instead."
(apply #'ebox-grid (append props nodes))) (apply #'ebox-grid (append props nodes)))
(t (t
(signal 'etaf-renderer-error (signal 'etaf-renderer-error
(list (format "Not a layout Host: %S" name)))))) (list (format "Not a layout Host: %S" name)))))))
(when range-child-p
(push node etaf--rendered-range-container-nodes))
node))
(defun etaf--lower-resolved-semantic-host
(name props content children range-child-p)
"Lower resolved NAME, PROPS, CONTENT, and CHILDREN through Renderer.
RANGE-CHILD-P preserves the direct material Range parent."
(pcase name
('text
(if children
(etaf--layout-node 'row props children range-child-p)
(apply #'ebox-create :content content props)))
('spacer (apply #'ebox-spacer props))
((or 'row 'column 'container 'stack 'flex 'grid)
(etaf--layout-node name props children range-child-p))
(_ (signal 'etaf-renderer-error
(list (format "Semantic Host requires Step4b lowering: %S"
name))))))
;;;###autoload ;;;###autoload
(defun etaf-render (view) (defun etaf-render (view)

File diff suppressed because it is too large Load Diff

View File

@ -23,12 +23,14 @@
(:constructor etaf--view-node-create)) (:constructor etaf--view-node-create))
"Internal normalized description of one Host View." "Internal normalized description of one Host View."
name name
token
props props
children) children)
(cl-defstruct (etaf--expr (cl-defstruct (etaf--expr
(:constructor etaf--expr-create)) (:constructor etaf--expr-create))
"Internal executable child expression." "Internal executable child expression."
token
thunk) thunk)
(cl-defstruct (etaf--component-spec (cl-defstruct (etaf--component-spec
@ -57,11 +59,13 @@
(:constructor etaf--slot-projection-create)) (:constructor etaf--slot-projection-create))
"A Component View request for one slot and its fallback children." "A Component View request for one slot and its fallback children."
name name
token
fallback) fallback)
(cl-defstruct (etaf--raw-ebox (cl-defstruct (etaf--raw-ebox
(:constructor etaf--raw-ebox-create)) (:constructor etaf--raw-ebox-create))
"Explicit escape carrying an already-built public Ebox node." "Explicit escape carrying an already-built public Ebox node."
token
thunk thunk
key-thunk) key-thunk)
@ -313,9 +317,11 @@ belong to the anonymous `default' slot."
(etaf--compile-child-form child slot-mode)) (etaf--compile-child-form child slot-mode))
children))) children)))
(if (eq slot-mode :projection) (if (eq slot-mode :projection)
(let ((token (gensym "etaf-slot-site-")))
`(etaf--slot-projection-create `(etaf--slot-projection-create
:name ',name :name ',name
:fallback (list ,@compiled)) :token ',token
:fallback (list ,@compiled)))
`(etaf--slot-input-create `(etaf--slot-input-create
:name ',name :name ',name
:children (list ,@compiled)))))) :children (list ,@compiled))))))
@ -328,8 +334,10 @@ belong to the anonymous `default' slot."
(defun etaf--compile-expr-form (items) (defun etaf--compile-expr-form (items)
"Compile an `expr' form with ITEMS into an executable View value." "Compile an `expr' form with ITEMS into an executable View value."
(let ((token (gensym "etaf-expr-site-")))
`(etaf--expr-create `(etaf--expr-create
:thunk (lambda () ,(etaf--parse-expr-form items)))) :token ',token
:thunk (lambda () ,(etaf--parse-expr-form items)))))
(defun etaf--compile-child-form (form &optional slot-mode) (defun etaf--compile-child-form (form &optional slot-mode)
"Compile structural child FORM into code returning a View value. "Compile structural child FORM into code returning a View value.
@ -380,11 +388,13 @@ SLOT-MODE distinguishes Component-owned projections from call-site inputs."
(unless (and (plist-member props :value) (unless (and (plist-member props :value)
(cl-every #'keywordp (cl-loop for (key _value) on props by #'cddr collect key))) (cl-every #'keywordp (cl-loop for (key _value) on props by #'cddr collect key)))
(etaf--syntax-error "Raw-ebox requires :value")) (etaf--syntax-error "Raw-ebox requires :value"))
(let ((allowed '(:value :key))) (let ((allowed '(:value :key))
(token (gensym "etaf-raw-site-")))
(dolist (key (cl-loop for (key _value) on props by #'cddr collect key)) (dolist (key (cl-loop for (key _value) on props by #'cddr collect key))
(unless (memq key allowed) (unless (memq key allowed)
(etaf--syntax-error "Raw-ebox does not accept %S" key))) (etaf--syntax-error "Raw-ebox does not accept %S" key)))
`(etaf--raw-ebox-create `(etaf--raw-ebox-create
:token ',token
:thunk (lambda () ,(plist-get props :value)) :thunk (lambda () ,(plist-get props :value))
:key-thunk ,(when (plist-member props :key) :key-thunk ,(when (plist-member props :key)
`(lambda () ,(plist-get props :key)))))))) `(lambda () ,(plist-get props :key))))))))
@ -395,12 +405,15 @@ SLOT-MODE distinguishes Component-owned projections from call-site inputs."
(host-view-p (eq (gethash (car form) etaf--view-registry) (host-view-p (eq (gethash (car form) etaf--view-registry)
etaf--host-marker)) etaf--host-marker))
(child-slot-mode (if host-view-p slot-mode :input))) (child-slot-mode (if host-view-p slot-mode :input)))
(let ((token (and (memq (car form) '(fragment etaf-fragment))
(gensym "etaf-fragment-site-"))))
`(etaf--view-call ',(car form) `(etaf--view-call ',(car form)
(list ,@(etaf--compile-property-plist props)) (list ,@(etaf--compile-property-plist props))
(list ,@(mapcar (lambda (child) (list ,@(mapcar (lambda (child)
(etaf--compile-child-form (etaf--compile-child-form
child child-slot-mode)) child child-slot-mode))
children))))))) children))
,(and token `',token)))))))
;;;###autoload ;;;###autoload
(defmacro etaf-view (form) (defmacro etaf-view (form)
@ -437,8 +450,8 @@ child region and accepts only `:value'."
key (etaf--component-spec-name spec))))) key (etaf--component-spec-name spec)))))
props)) props))
(defun etaf--view-call (name props children) (defun etaf--view-call (name props children &optional token)
"Construct a Host or Component named NAME from PROPS and CHILDREN." "Construct NAME from PROPS and CHILDREN, retaining optional site TOKEN."
(unless (symbolp name) (unless (symbolp name)
(etaf--syntax-error "View name must be a symbol: %S" name)) (etaf--syntax-error "View name must be a symbol: %S" name))
(setq props (etaf--validate-property-plist props)) (setq props (etaf--validate-property-plist props))
@ -450,6 +463,7 @@ child region and accepts only `:value'."
(etaf--resolve-property-value (plist-get props :key)))) (etaf--resolve-property-value (plist-get props :key))))
(etaf--view-node-create (etaf--view-node-create
:name (etaf--canonical-host-name name) :name (etaf--canonical-host-name name)
:token token
:props props :props props
:children children)) :children children))
((etaf--component-spec-p entry) ((etaf--component-spec-p entry)

View File

@ -0,0 +1,7 @@
# Core boundaries after the `.etaf` pair experiment
The safe data-only `.etaf` pair path was removed from current core. It added a second file grammar, companion execution boundary, and registry validation surface before a compiler contract existed. The archived `emacs-box` DSL cannot be reused as a shortcut: it has different ownership and executable semantics. A future `.etaf` SFC belongs in a compiler layer that emits the existing `etaf-view` and `etaf-define-component` contracts.
The current Component definition remains the single structure/style/behavior unit. Its `:view` owns structure, `:styles` owns presentation, and `:setup` owns retained state, Actions, and lifecycle. Style scopes now stop at nested Component internals; caller-owned slot content keeps the caller scope, while child-authored slot fallbacks keep the child scope.
Mounted input stays in the small Runtime lifecycle path: the input minor mode is enabled after a successful mount and disabled before unmount. TAB/backtab, RET, and mouse-1 use the public focus and hit-test operations rather than a second event layer.

View File

@ -0,0 +1,7 @@
# 移除 `.etaf` pair 后的 core 边界
当前 core 移除了只读 data `.etaf`、同名 `.el` companion 的 pair 主路径。它在 compiler 契约尚未存在时增加了第二套文件语法、companion 执行边界和 registry 校验面。不能把 `emacs-box` 的归档 DSL 当捷径复用:它的所有权和可执行语义不同。未来 `.etaf` SFC 应属于 compiler layer并输出既有的 `etaf-view``etaf-define-component` 契约。
当前 Component definition 仍是唯一的结构/样式/行为单元:`:view` 负责结构,`:styles` 负责 presentation`:setup` 负责 retained state、Action 和生命周期。样式 scope 现在在嵌套 Component 内部边界停止;调用者提供的 slot 内容保留 caller scope子组件自有的 slot fallback 保留 child scope。
挂载后的输入仍走窄的 Runtime 生命周期路径:成功 mount 后启用 input minor modeunmount 前禁用。TAB/backtab、RET 和 mouse-1 复用公共 focus/hit-test 操作,不建立第二套事件层。

File diff suppressed because it is too large Load Diff