Skip to content

fix(core): support Stage-3 clone decorators - #3078

Open
luzhuang wants to merge 3 commits into
feat/migrate-2.0from
fix/migrate-stage3-clone-decorators
Open

fix(core): support Stage-3 clone decorators#3078
luzhuang wants to merge 3 commits into
feat/migrate-2.0from
fix/migrate-stage3-clone-decorators

Conversation

@luzhuang

@luzhuang luzhuang commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

为什么要改

Gravity 按 TypeScript Stage-3 语义编译字段装饰器,调用协议是
(undefined, ClassFieldDecoratorContext)。Engine 原实现使用另一套装饰器 ABI,把第一个参数
当作 prototype,因此迁移脚本在模块求值阶段就会把 undefined 传入克隆元数据注册并报错。

最新 feat/migrate-2.0 的克隆行为由 CloneDecorators、字段模式元数据和现有 clone
traversal 共同拥有。本 PR 直接统一到标准 Stage-3 语义,不保留旧 ABI 的重载、运行时判型或
平行元数据路径。

设计

  • deepCloneassignmentCloneignoreClone 只接受标准
    ClassFieldDecoratorContext
  • 装饰字段在类声明求值时写入标准 context.metadata;每个类声明只拥有自己的字段模式表,
    不依赖实例构造时序
  • clone consumer 沿真实 constructor 继承链解析声明级元数据;解析结果只做派生缓存,不写入
    实例或 concrete leaf prototype
  • 不再存在 initializer 注册、WeakSet 去重、prototype 元数据副本或第二套 registry
  • 基类字段模式与子类重饰均由声明继承顺序决定,子类覆盖不会污染基类,也不会被基类构造过程
    反向覆盖
  • clone traversal 只消费 own enumerable string keys,因此只支持 public、非 static、
    string-named instance field;private、static、symbol、method 与 accessor 均显式拒绝
  • 删除 64 个方法上的 @ignoreClone:方法位于 prototype,从未进入字段遍历,这些标注没有
    可观察语义

Gravity 与编译链路

  • Editor Gravity 当前实际加载 @babel/standalone@7.24.7,并以
    proposal-decorators { version: "2023-11" } 转换脚本;实测产物会向字段 decorator
    context 传入 metadata,并在子类转换时传递父类
  • 删除全部 package/test tsconfig 中的 experimentalDecorators
  • Rollup/SWC 显式使用 2022-03 标准装饰器转换和标准 class-field 语义
  • clone consumer 以 JavaScript constructor chain 表达继承,不把某个编译器是否链接 metadata
    object prototype 当作语义前提
  • Browser Vitest 使用 ES2022 target,确保测试执行的也是标准装饰器转换
  • Shader Compiler AST 不再依赖类装饰器:98 个节点改为声明式 static nonTerminal,对象池由
    TreeNode 统一惰性创建;映射已与原 98 项逐项比对一致

TDD 与验证

  • 红:删除 experimentalDecorators 后,64 个无效方法装饰器立即产生类型错误;浏览器未
    downlevel 时产生装饰器语法错误;使用 context.addInitializer 时出现 48 个克隆、Camera、
    引用计数与 Physics 连锁失败
  • 评审红:声明级 owner 断言在 prototype 注册实现上失败 2/109;只依赖 metadata object
    prototype 又在 SWC 产物中触发 10 个文件、31 个测试失败
  • 绿:真实装饰器语法覆盖声明级元数据归属、多实例、继承覆盖和非法目标拒绝
  • pnpm run b:module:通过
  • pnpm run b:types:通过
  • pnpm lint:0 error
  • pnpm exec vitest run:135 files、1758 tests 全部通过
  • Clone.test.ts:109/109
  • 生成声明只暴露 ClassFieldDecoratorContext;构建产物只使用标准装饰器 helper

发布边界

0.0.0-experimental-2.0-migrate.5 已从更早实现发布,本 PR 不复用或重发该版本;包含本修复
的后续发布必须使用新版本。

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • ^(dev/)?\d+.\d+$

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5fbb1a08-e737-413e-826f-efe64e5443aa

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/migrate-stage3-clone-decorators

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jul 28, 2026

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🫧 尘小沫

结论

阻塞级别:P1(2 项)。本轮对 4dab08da528bb5284b17afdfbabfe3f95aa7832b...51dffa3d711578a314f26f89e665d160400bbb49 的两个增量 commit 做了首次完整审查,实际 review 动作为 REQUEST_CHANGES,目标 HEAD 为 51dffa3d711578a314f26f89e665d160400bbb49。Stage-3 字符串字段的基本路径已经接通,但字段 key 契约和 metadata owner 尚未收口。自动 CR 不替代人工 Reviewer 的合入门禁,修复后仍需人工 Reviewer 审核确认。

问题

  • [P1] Stage-3 overload 接受了 clone traversal 永远不会访问的字段packages/core/src/clone/CloneManager.ts:14-17, 51-80;下游 packages/core/src/clone/CloneUtil.ts:23-30

    ClassFieldDecoratorContext.name 合法地包含 symbol,并且 Stage-3 也允许装饰 private field;当前 overload 对 @assignmentClone [symbol]@assignmentClone #privateField 都能通过 TypeScript 5.6 类型检查,注册表也会写入对应 key。但下游仍以 Object.keys(source) 作为唯一字段集合,只会返回 enumerable string keys;symbol 字段完全不会进入 clone,private field 也不可能由该路径访问。实际结果是装饰器静默失效,例如 symbol-keyed @assignmentClone 字段在 clone 上保留 constructor preset/undefined,而不是共享 source 值。请统一公开契约与消费域:若支持 symbol,就由 CloneUtilReflect.ownKeys 加 enumerable 过滤遍历,并补一条 symbol-keyed Stage-3 回归测试;private field 无法纳入当前 enumerable-field clone 模型,应读取 context.private 并在装饰阶段明确拒绝,不能静默登记一个永远消费不到的 mode。

  • [P1] 类级 clone metadata 被持久化成每个 Stage-3 实例一份packages/core/src/clone/CloneManager.ts:35-36, 95-104

    legacy 路径把 _fieldModes 写在 class prototype;Stage-3 initializer 却把 this 直接传给 _registerFieldMode,因此每构造一个带 clone decorator 的迁移组件,都会分配一个新的 _fieldModes 对象并重复写入同一组 class-invariant modes。clone source 与 constructor-built target 还会各持一份。这既与 96 行“Each class gets its own _fieldModes”的现行契约矛盾,也把 owner 从“一类一份”扩成“一实例一份”,给场景加载/批量实例化增加无必要的对象分配和 metadata 同步面。应保留 class prototype 作为唯一权威 owner:initializer 只负责取得实例原型(例如 Object.getPrototypeOf(this))并注册到该原型,删除实例自有 _fieldModes;测试至少用同类两个实例验证行为共用同一原型表且实例本身没有镜像表。

架构、熵增与测试治理

直接链路的上游 owner 是 TypeScript/Gravity 提供的 decorator context,写入 authority 是 CloneManager,下游唯一消费者是 CloneUtil._deepCloneObject。改动前 metadata 只有 prototype 一种落点、字段域只有 enumerable string key;改动后落点净增为 prototype + per-instance 两种,注册 key 域扩成 string | symbol,消费域却仍停在 string,形成了第二种持久状态和一条断裂协议。应保留 CloneManager 的 prototype registry 与 CloneUtil 的 enumerable-own-field traversal 作为唯一 owner/协议:删除 per-instance registry;让 traversal 完整消费 enumerable symbol,无法消费的 private field 在入口拒绝。legacy/Stage-3 的调用分支是真实编译边界,可以保留,但两条分支必须汇入同一个 metadata owner,不能各自维持一种存储形态。

新增测试只构造了 name: string 的 mock context、只创建一个实例,因此同时漏掉 symbol/private 的负向契约和 per-instance 镜像状态;它没有引入为了旧测试保留的 production fallback,既有 legacy 测试也仍有效,无需删除。修复时应扩展这组新契约测试,而不是再增加 symbol/private fallback、第二套 registry 或额外转换层。19 个 package version 变更为同一 experimental 版本的机械同步,未发现第三份版本状态或旧 fixture/snapshot 残留。

@luzhuang luzhuang removed the documentation Improvements or additions to documentation label Jul 28, 2026
@luzhuang
luzhuang force-pushed the fix/migrate-stage3-clone-decorators branch from 51dffa3 to 2b82411 Compare July 28, 2026 09:46
@luzhuang
luzhuang force-pushed the fix/migrate-stage3-clone-decorators branch from 2b82411 to 4e9925c Compare July 28, 2026 09:58

Copy link
Copy Markdown
Contributor Author

已按最新 feat/migrate-2.0@f6da19d79 重写,当前 head:4e9925cd6f629ea56cd6c13ae720c7648771badf

  • 删除基于退休 CloneManager 的旧实现,直接适配当前 CloneDecorators / 原型 symbol 元数据架构
  • legacy 与 Stage-3 共用唯一 CloneMetadata.registerFieldMode 路径;不写实例元数据,不增加第二套 registry
  • private/static/symbol/non-field context 在装饰器边界显式拒绝
  • 增加多实例、继承覆盖和拒绝边界测试
  • PR 中不包含工程 note

本地验证:Clone.test.ts 112/112;全量 135 files / 1761 tests;b:moduleb:types、lint(0 error)通过。请基于当前 head 重新 review。

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🫧 尘小沫

结论

阻塞级别:P1(1 项)。本轮基于现行 feat/migrate-2.0@f6da19d79673cfe8f8a5ffa739a82a7ebdeb4619...4e9925cd6f629ea56cd6c13ae720c7648771badf 的 1 个 commit、2 个文件做了完整增量审查,并复核了上一轮两项 P1;实际 review 动作为 REQUEST_CHANGES,目标 HEAD 为 4e9925cd6f629ea56cd6c13ae720c7648771badf。字段域拒绝和 prototype metadata owner 已基本收口,但 legacy / Stage-3 混合继承时,最派生字段 override 仍会被基类 initializer 覆盖。自动 CR 不替代人工 Reviewer 的合入门禁,修复后仍需人工 Reviewer 审核确认。

已关闭问题清单

  • 已修复:Stage-3 overload 接受 traversal 无法消费的 symbol/private 字段。 4e9925cd6f629ea56cd6c13ae720c7648771badfCloneDecorators.ts:42-44 于装饰器边界拒绝 private、static、symbol 和 non-field context,并在 Clone.test.ts 增加对应负向测试;无效 key 不再进入 registry。
  • 已修复:类级 metadata 被持久化成每个 Stage-3 实例一份。 同一 commit 的 initializer 改为向 Object.getPrototypeOf(this) 注册,CloneMetadata 只在 prototype 上定义 fieldCloneModesKey;多实例测试也验证两个实例没有新增自有 symbol。此项关闭仅针对上一轮的实例镜像问题,不覆盖下述混合继承注册次序问题。

问题

  • [P1] Stage-3 基类 initializer 会覆盖 legacy 子类已登记的同名字段 overridepackages/core/src/clone/CloneDecorators.ts:46-54, 109-118;下游 packages/core/src/clone/ComponentCloner.ts:15-24

    当前 legacy 路径在类求值时把 mode 写到声明类 prototype,而 Stage-3 路径要等实例构造,再把 mode 写到 Object.getPrototypeOf(this)。因此当 Stage-3 编译的基类用 @assignmentClone target、legacy 编译的子类用 @ignoreClone target 时,legacy decorator 会先把 Ignore 正确写到子类 prototype;第一次 new Sub() 又执行基类的 Stage-3 initializer,此时 Object.getPrototypeOf(this) 仍是 Sub.prototype,第 51 行会把该 own entry 无条件改回 Assignment,随后 WeakSet 还会把这个错误结果固定下来。ComponentCloner 最终读取到基类 mode,clone 会共享 source 引用,而不是保留子类 constructor preset。我用 TypeScript 5.6.3 的真实 Stage-3 emit 反向验证,表项在构造前为 ignore、构造后变为 assignment。现有继承测试只模拟 Stage-3 → Stage-3;子类 initializer 随后再次覆盖,因此恰好遮住了混合协议方向。

    请保留“每个声明类一张 fieldCloneModesKey 表”作为唯一权威 owner,让 Stage-3 注册锚定装饰字段的声明类,而不是运行时实例的最派生 prototype(可使用 Stage-3 的声明级 metadata carrier 或等价机制解析声明 owner)。随后删除 concrete leaf prototype 上机械复制的基类 mode 和为这些副本服务的 registeredPrototypes 镜像;下游继续只沿声明类 metadata 继承链读取最终 mode。测试至少补齐 legacy base → Stage-3 sub 与 Stage-3 base → legacy sub 两个同名字段 override 方向,尤其要让后者在修复前失败;不要用兼容分支在 clone consumer 侧二次猜测优先级。

架构、熵增与测试治理

这条链路的上游协议 owner 是 TypeScript decorator context,写入 owner 是 CloneMetadata.registerFieldMode,下游只由 ComponentCloner / CloneUtil 消费 fieldCloneModesKey。改动前 legacy decorator 在声明期向声明 prototype 写一次,继承优先级完全由 prototype 链决定;改动后 Stage-3 每个字段净增一个实例 initializer、一个 WeakSet,并把基类 mode 复制到每个实际构造过的最派生 prototype。纯 legacy 与纯 Stage-3 两端各自成立,但混合后注册时间参与优先级,形成了第二套时序规则,且可覆盖更派生声明的权威事实。

应收口回声明类 metadata:入口可以保留 legacy / Stage-3 两种调用协议,但两者都只登记各自声明 owner;consumer 机械沿同一继承链取值。这样可删除 leaf 副本、WeakSet 注册镜像和任何下游优先级补偿。上一轮失效的单实例 mock 已按新公开契约重写,多实例与拒绝边界测试有效,无需删除;本轮没有发现为了旧测试保留的 production compatibility branch、legacy fallback、fixture 或 snapshot,但新增继承 fixture 仅覆盖同协议路径,必须补混合协议反向测试,不能让生产代码迁就现有测试形态。

@luzhuang

Copy link
Copy Markdown
Contributor Author

感谢指出旧 HEAD 中 Stage-3 基类 initializer 会覆盖派生类声明结果的问题。这个 P1 成立:
4e9925cd6 的 metadata owner 实际受实例构造时序影响,不是真正的声明级 owner。

当前 HEAD aa7a1a077 已按声明所有权重新收口:

  • 删除旧 decorator ABI,仅保留标准 Stage-3 ClassFieldDecoratorContext,因此不再存在两套
    decorator 协议的混合继承状态;
  • 字段模式在类声明求值时直接登记到 context.metadata,不再返回 initializer;
  • 删除 registeredPrototypes / WeakSet,不向实例或 concrete leaf prototype 复制基类模式;
  • CloneUtil / ComponentCloner 从实际 constructor 继承链读取每个声明自己的 metadata,
    子类 own mode 机械覆盖父类 mode;
  • private、static、symbol、method、accessor 仍在入口明确拒绝。

我也用 Editor Gravity 实际加载的 @babel/standalone@7.24.7 +
proposal-decorators { version: "2023-11" } 验证了转换结果:字段 context 会携带
metadata,子类 helper 会接收父类。Engine 侧另外不依赖 SWC 是否链接 metadata object
prototype,而以真实 constructor chain 作为继承事实。

验证结果:Clone.test.ts 109/109;全量 135 files / 1758 tests;b:module
b:types、lint(0 error)均通过。请基于 aa7a1a077 重新 review。

GuoLei1990

This comment was marked as outdated.

@luzhuang

Copy link
Copy Markdown
Contributor Author

这项 P2 我重新按仓库真实编译边界验证后不采纳。

declare 确实可以避免 class field 初始化覆盖 decorator 写入的 prototype.nt /
静态 pool,但它没有解决本仓实际阻塞:这些节点是
export namespace ASTNode 内的 decorated export class。我取
feat/migrate-2.0 的完整 AST.ts,只按建议把 TreeNode.nt /
TreeNode.pool 改成 type-only declare,然后使用仓库锁定的
@swc/core@1.9.2 和 PR 中的两套精确配置做转换:

  • runtime:2022-03 + useDefineForClassFields + loose + ES5
  • bundler:2022-03 + useDefineForClassFields + ES2020

两条路径都会在 SWC transform 阶段失败:

ESM-style module declarations are not permitted in a namespace.

因此恢复 98 个 class annotations 会让 b:module 使用的编译阶段重新失败;问题不在
class-field 覆盖,而在 SWC 对 TypeScript namespace 内 decorated export declaration
的支持边界。

当前实现也没有两份 non-terminal 映射:98 个 static nonTerminal 与原来的 98 个
decorator 参数是一一替换,TreeNode.nt 直接读取这份声明。两个 pool 容器承担不同职责:
TreeNode._pools 负责 constructor → pool 查找,
ShaderCompilerUtils._shaderCompilerObjectPoolSet 持有同一批 pool 以统一
clear/release,并不存在两份 pool authority。

所以保留当前声明式 static nonTerminal + TreeNode 统一 pool 实现;不恢复
experimentalDecorators、旧 decorator ABI 或额外兼容分支。

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🫧 尘小沫

结论

阻塞级别:无。本轮基于 feat/migrate-2.0@f6da19d79673cfe8f8a5ffa739a82a7ebdeb4619...aa7a1a077b46e3e1331f9709ca0e3138c7093769 的现行 53 文件稳态完成增量复审;相较上一份同 HEAD review,GitHub compare 为 0 个 commit、0 个文件,实际增量是作者对 Shader AST P2 的编译边界说明。本轮未发现新的 P0/P1/P2,实际 review 动作为 COMMENTED,目标 HEAD 为 aa7a1a077b46e3e1331f9709ca0e3138c7093769。作者对剩余 P2 的解释经仓库锁定 SWC 版本与真实配置复核成立。自动 CR 不替代人工 Reviewer 的合入门禁,仍需人工 Reviewer 审核确认。

已关闭问题清单

  • 已修复:Stage-3 decorator 接受 traversal 无法消费的 symbol/private 字段。 4e9925cd6f629ea56cd6c13ae720c7648771badf 已在入口拒绝 private、static、symbol 和 non-field context;当前 HEAD 的真实标准装饰器测试继续覆盖四类非法目标,字段域与 Object.keys consumer 一致。
  • 已修复:类级 clone metadata 被持久化成实例或 concrete leaf prototype 镜像。 aa7a1a077b46e3e1331f9709ca0e3138c7093769 只在类声明求值时写 context.metadata,旧 initializer、registeredPrototypes / WeakSet、实例与 leaf prototype 副本均已删除。
  • 已修复:基类注册时序覆盖派生类同名字段 override。 2a88dcf19443120759f1e55bfce3fe15e69516e6 删除 legacy decorator ABI,aa7a1a077b46e3e1331f9709ca0e3138c7093769getFieldCloneModes 沿真实 constructor chain 合并各声明 own metadata;继承优先级不再依赖实例构造时序。
  • 作者解释成立:Shader AST 应保留当前 static nonTerminal + 统一 pool 方案。 我用 @swc/core@1.9.2 对 base AST.ts 分别执行 PR 的 runtime(2022-03 + useDefineForClassFields + loose + ES5)与 bundler(2022-03 + useDefineForClassFields + ES2020)配置,两者均在 namespace 内 decorated export class 处报 ESM-style module declarations are not permitted in a namespace;当前 HEAD 在两套配置下均转换成功。base 的 98 个 decorator 参数与当前 98 个 static nonTerminal 一一替换,后者是唯一 non-terminal 映射;TreeNode._pools 提供 constructor → pool 查找,ShaderCompilerUtils._shaderCompilerObjectPoolSet 只枚举同一批 pool 以统一 clear/release,二者不是重复 authority。上一轮 P2 的删除方案在真实编译边界不可行,本项关闭。

架构、熵增与测试治理

clone 链路的上游协议 owner 是 Babel/SWC 提供的标准 decorator context,唯一写入 owner 是各声明的 context.metadatagetFieldCloneModes 沿 JavaScript constructor chain 机械生成只读派生缓存,下游仍只有 CloneUtilComponentCloner 两个 consumer。相较旧路径,legacy ABI、实例 initializer、注册 WeakSet、prototype/leaf 镜像均已删除,owner、持久状态和兼容分支净减少;真实语法测试覆盖声明 metadata、继承 override 与非法目标,未发现为旧测试保留的 production fallback、wrapper、镜像状态或失效 fixture/snapshot。

Shader AST 链路由每个节点的 static nonTerminal 保有唯一语法事实,TreeNode.nt 机械读取该事实;静态 pool getter 通过 WeakMap 为 constructor 建立唯一 pool,createObjectPool 再把同一对象登记到统一 clear/release 列表,下游 CFG / GrammarUtils 继续直接消费 Node.pool。映射数量保持 98 → 98;新增索引替代了受 SWC namespace 限制而无法保留的 decorator side effect,没有形成第三份真相、平行状态机或 legacy/compat 路径。当前 HEAD 已通过两套真实 SWC transform 边界,现有 parser 与 clone 契约测试无需为旧实现增加兼容逻辑。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants