返回博客
updaterdevtoolsrefactorcommunitymilestone

自动更新、实时 Inspector,以及把自己砍掉一半的编译器

上一篇文章在 v0.5.306 收尾,主线是 gen-GC + JSON + 基准。四天后,Perry 已经到了 v0.5.359 — 也就是 53 个补丁版本 — 故事又变了。这些版本里没有一个是以基准数字作为头条。几乎全部都是 跟踪器中的 issue 被关闭

  • perry/updater 上线 — 桌面应用的 Sparkle / Tauri 风格自动更新(对 SHA-256 摘要做 Ed25519 签名、哨兵回滚、分离重启)。社区 PR,由 TheHypnoo 贡献(#224)。
  • Geisterhand 阶段 Dhttp://localhost:7676 上的实时 inspector,包含 widget 树、按 widget 详情、点击分发、以及通过 POST /style/:h 实时编辑样式。
  • 编译器重构。 在 v0.5.329 → v0.5.343 期间,最常被提到的 4 个文件被切分:lower::lower_expr 6,687 → 624 LOC(−91%)、compile.rs 9,391 → 3,783 LOC(−60%)、lower.rs 13,591 → 7,554 LOC(−44%)、lower_call.rs 7,000+ → 4,681 LOC(−33%)。新的 walker.rs_ => 通配 catch-all 这一类 bug 转成编译期错误。
  • UI 样式阶段 C 收官 — 在 Apple、Android、GTK4、Windows 和 Web 的每个 widget 上都支持内联 style: { ... } 属性。Windows 接通了 5 个桩中的 4 个(decoration / opacity / borders);只剩 widget.shadow(DirectComposition 后续跟进)。
  • 面向 Windows 的 Scoop bucketscoop install perry-ts/perry。release 工作流加入 SHA-256 sidecar。
  • 一波社区 issue 修复 — 在 runtime、codegen、fetch、GTK4、Windows linker、async、stdlib 等维度共关闭约 30 个 issue。

1. perry/updater — 桌面应用自动更新

修复前 Perry 没有任何更新路径。应用打包发布、再发布,仅此而已。TheHypnoo#224 提供了完整方案:

import { initUpdater, checkForUpdate, markHealthy } from "@perry/updater";

initUpdater(); // 上次启动崩溃时执行哨兵回滚

const update = await checkForUpdate({
  manifestUrl: "https://example.com/updates/manifest.json",
  publicKey: "<ed25519 raw 32-byte hex>",
  currentVersion: "1.4.0",
});

if (update) {
  await update.download((pct) => console.log(`${pct}%`));
  await update.installAndRelaunch();
}

markHealthy(); // 在新构建成功启动后调用

信任模型:对文件的 SHA-256 摘要做 Ed25519 签名(不是对文件字节签名 — 大二进制下验证仍然便宜)。manifest 是 JSON,带 schema 版本,每个 <os>-<arch> 三元组一条记录。原子安装并保留 <exe>.prev 备份,分离重启(Unix 上 setsid,Windows 上 DETACHED_PROCESS)。移动端按设计排除 — App Store / Play Store 在 OS 层面拥有安装管线。

编写冒烟测试时浮现出 Perry 运行时的两个怪癖,并被一并修掉:

  • response.arrayBuffer() 之前只返回元数据 stub。#232 修复(同样是 TheHypnoo) — js_response_array_buffer 现在分配真正的 BufferHeader,并把 resp.body 通过 memcpy 拷贝进去。
  • fs.appendFileSync 写入了 0 字节。#226 修复 — 命名空间导入的 lowering 路径(import * as fs from "fs")没有 appendFileSync 的分支,LLVM codegen 也没有对应 HIR 变体的分支。两边都已接通。

文档位于 docs/src/updater/overview.md

2. Geisterhand:localhost:7676 上的实时 inspector

Geisterhand 一直是 Perry 的进程内 UI 测试工具 — 端口 7676 上的 HTTP API,用来快照 widget 状态、分发点击。阶段 D 把它变成可以从任何浏览器打开的 devtools 风格 inspector。

  • 步骤 1(v0.5.349)GET / 提供一个单页 vanilla-JS UI,包含 widget 树、按 widget 的详情(frame、value、原始 JSON)、1.5 秒自动刷新(可暂停/恢复),以及一个“触发 onClick”按钮。codegen 针对 macOS 的延迟加载 -dead_stripINSPECTOR_HTML 钉死,确保它在 release 构建中存活下来。
  • 步骤 2(v0.5.350)POST /style/:h 接收一袋 JSON 属性并实时应用。9 个属性(backgroundColorcolorborderColorborderWidthborderRadiusopacitypaddinghiddenenabled)通过现有 pump-queue 从 HTTP 线程 → 主线程流转。坏 JSON → 400;坏 handle → 400;未知属性在服务端被过滤,响应里列出哪些通过了。
perry compile main.ts -o app --enable-geisterhand
./app &
open http://localhost:7676
curl -X POST localhost:7676/style/3 \
  -H 'content-type: application/json' \
  -d '{"backgroundColor":"#1a1a1e","opacity":0.8}'
# => {"ok":true,"applied":["backgroundColor","opacity"]}

macOS 派发器已接通;Linux / Windows / iOS / tvOS / visionOS / Android 走相同形态,是接下来要做的。

3. 编译器重构 — 切开四个最大的文件

跟踪器中的 5 个 issue(#167#169#212#214,加上长尾)形态相同:往 ir.rs 加了一个新的 Expr 变体,但 lower.rs 中四个临时 walker 之一带有 _ => 通配,悄悄把新变体编译错了。在运行时抓这种问题代价昂贵 — 有时是无形的,有时是 SSO 下的 SIGSEGV。

v0.5.329 引入 crates/perry-hir/src/walker.rs,提供 walk_expr_children / walk_expr_children_mut — 对全部 178 个 Expr 变体的穷尽 match,不带 catch-all。增加新变体而不在这里登记,现在直接是编译错误。四个消费者(substitute_localsfind_max_local_id::check_exprcollect_local_refs_exprremap_local_ids_in_expr)随之收缩:

函数Δ
find_max_local_id::check_expr22557−75%
substitute_locals55380−86%
collect_local_refs_expr72070−90%
remap_local_ids_in_expr54285−84%

合计:消除 1,830 行重复 descent,被 新增 1,840 行集中式 walker 取代 — 净增减为零,但这一类 bug 没了。

随后其他工作得以推进。v0.5.331 → v0.5.343 在 14 次提交里切开了四个巨石。头条数字:

文件Δ
lower::lower_expr6,687624−91%
compile.rs9,3913,783−60%
lower.rs13,5917,554−44%
lower_call.rs7,000+4,681−33%

切分落地为 19 个聚焦的子模块:compile/{parse_cache, strip_dedup, library_search, object_cache, resolve, collect_modules, optimized_libs, targets, link}.rslower/{expr_misc, expr_function, expr_object, expr_call, expr_member, expr_assign, expr_new}.rslower_call/{ui_styling, builtin, native}.rs,外加新的 crates/perry-dispatch crate,成为 UI / system / i18n 方法表的单一事实源(曾导致 issue #191“在 macOS 上能编译,在 web 上崩”那种意外的 _ => "perry_ui_unknown" 扇出,现在就一次查表)。

Tier 4 性能优化 同行(v0.5.335–v0.5.336):

  • inline_functions 中两个 pass 与 compile.rs 中三个 rayon pass 合并 — 每次编译节省 5 次模块扫描和 3 次调度往返。
  • perry dev 的解析缓存限制为 500 条、FIFO 淘汰。修复前一个遍历 node_modules 的会话能持有 100+ MB 的 SWC AST。
  • 把 codegen 后的 .ll 写入循环并行化 — 在含 50+ 模块的 SSD 上 wall-time 快 2–4 倍。
  • Arc<I18nTable> 取代每个 worker 都克隆 locale 表。

Workspace 测试每次提交都保持 434 passed / 0 failed / 5 ignored;gap 测试维持基线 25/28;doc-tests 维持基线 80/82。

4. UI 样式阶段 C,收尾

阶段 C 是内联 style: { ... } 的铺开。本时间窗内步骤 1–7 收口:

  • v0.5.305 → v0.5.306StyleProps 类型表面 + Button 上的内联 style:
  • v0.5.307 → v0.5.309 — 每个表格类 widget 上的 color/padding/shadow 内联解构,随后是 VStack / HStack。
  • v0.5.310 → v0.5.311 — 十六进制字符串 + 渐变 + 用于动态值的运行时 parseColor
  • v0.5.312 — 样式文档 + Windows 跟踪 issue。

之后是跨平台扫尾:

  • GTK4#202#206) — 接通 4 个样式 FFI,外加阻塞 Linux doc-tests gate 的 7 个缺失 FFI(v0.5.322)。
  • macOS(v0.5.324) — 为 widget.shadow 接通 CALayer 阴影管道 + visual_test 基础设施;为非 NSTextField widget 加上 set_color 类探测。
  • iOS / tvOS / visionOS(v0.5.346) — Button 的 color: ... 之前在 UIButton 上调用 setTextColor:,但它没有实现这个 selector;objc2 的 panic 跨过 extern "C" 边界,进程被中止。沿用 macOS 同款类探测模式修复 — UIButton 现在走 setTitleColor:forState:UIControlStateNormal
  • Windows(v0.5.347) — 接通了 5 个样式桩中的 4 个(text.decorationLOGFONT round-trip,widget.opacityWS_EX_LAYERED + SetLayeredWindowAttributes,borders 走 SetWindowSubclass + WM_PAINT)。只剩 widget.shadow(需要 DirectComposition)。

docs/src/ui/styling-matrix.md 中的样式矩阵在窗口结束时为:Web 43/43 已接通Windows 42/43 已接通,其余覆盖完整。

5. 运行时正确性 pass — 一个 issue 一个 issue 地

本时段的主题:每个通过跟踪器进来的错误编译,要么变成修复,要么变成编译期错误。亮点:

  • #212(v0.5.323)fn 内的类方法无法捕获包裹 fn 的 local。多模块复现现在与 Node 字节级一致。
  • #214(v0.5.321 + v0.5.330) — 7 处字符串操作数点位的 SSO 安全 string-handle 拆箱:arr.joinarr.toStringobj[stringKey] 的 get/set/delete、string.match(re)process.env[dynKey]、crypto digest 输入。修复前每一处对内联字符串操作数都要么悄悄返回垃圾,要么直接 SIGSEGV。
  • #221(v0.5.351) — 模块级别为空的 const 数组从函数内部接收的 arr[i]= 写入会丢失。Bloom-Engine/jump 的 discoverLevels() 在模块级别用 index-assign 填 LEVEL_FILES,关卡选择界面渲染为空时浮现。
  • #233(v0.5.357) — 当数组以参数形式传入 async 函数时,Array.push 会被悄悄限制在 16 个元素。async 函数不会被内联;重新分配返回的新指针调用方看不到。修复:每次扩容都在旧位置安装一个 forwarding 指针,复用 GC 现有的 GC_FLAG_FORWARDED 机制。
  • #235(v0.5.358) — 调用方省略尾部参数时,方法默认参数派发会传垃圾。两处共谋:跨模块方法声明硬编码 6 个 double,而不是 arity + 1;同时 lower_class_method 根本没调用 build_default_param_stmts。在 mongodb 的 findOne(filter, options = {}) 静默挂起时浮现;修复在本地与跨模块派发上是统一的。
  • #236(v0.5.355) — 来自一个复现的三个独立 fetch + promise bug:api.github.com 对匿名请求返回 403(现在设置默认 User-Agent),.then(console.log) 永久挂起(null 回调没有把 TASK_QUEUE 条目入队),每个 fetch 拒绝都打印 Uncaught exception: [object Object](NaN-box 的裸 *StringHeader 而不是真正的 ErrorHeader)。
  • #234(v0.5.359) — 拥有 arrayBuffer / text / bytes / slice 实例方法的真实 Blob。修复前 await response.blob() 只返回元数据 stub {size, type}。三部分修复落地于 runtime + HIR + codegen。

外加一些小补:

  • #181 — strip-dedup 在 Linux 上过度修剪泛型单态化 + GTK4 链接静默 fallback。修复:用通过 llvm-nm符号集合比较替代名称模式过滤。哪怕只有一个独有符号的成员都保留。在没有链接错误的情况下把 libperry_ui_macos.a 从 196 → 35 个对象。
  • #220 — 给 Windows 链接行加上 secur32.lib
  • #198 — i18n FormatNumber 通过 Ryū 进行 FP round-trip。
  • #188 — 为 perry/i18n 的格式包装器接通 codegen 派发。
  • #189 / #203perry/plugin codegen 派发。
  • #190 — Canvas widget 通过 LLVM codegen。
  • #191 — CameraView 通过 codegen。
  • #192 — Table widget 通过 codegen。
  • #193(部分) — 11 个 stdlib helper 派发分支。
  • #98 — iOS + Android 的后台接收通知(warm-path)。
  • #106 — watchOS 游戏循环 FFI 钩子的弱回退。
  • #154using / await using 的 dispose 钩子。
  • #167js_native_call_method 的参数 alloca 提升到 entry 块。
  • #169substitute_locals 的 Uint8Array 分支。
  • #226fs.appendFileSync 端到端接通(社区 PR)。

6. Windows + Scoop

Windows 工具链故事在持续简化。v0.5.353 在 host 构建中对 clang -target 进行钉死 — PATH 上的非 MSVC clang(MinGW / MSYS2 / Anaconda / Rust GNU bundle)会悄悄把 Perry 的 x86_64-pc-windows-msvc IR 改写成 windows-gnu,lld-link 无法解析 LLVM 的 mingw32 emitter 插入的 __main 引用。新增的 probe_clang_default_triple 每个进程跑一次 clang --version,当 host 默认是 GNU 但我们目标是 MSVC 时打印一行信息提示。可用 PERRY_NO_CLANG_PROBE=1 抑制。

v0.5.345 把 Win64 上的 perry-ui ABI 与 perry-dispatch 对齐 — 三个运行时 extern 签名漂移了(perry_ui_navstack_createperry_ui_menu_add_item_with_shortcutperry_ui_app_set_timer)。Win64 ABI 中整型与浮点位置参数共享槽位索引,因此不匹配时会从未初始化寄存器读到垃圾。SysV(macOS / Linux)使用分离的 int/float 寄存器池,恰好落上有效位 — 仅 Windows 崩溃,已在 8 个 perry-ui-* 平台 crate 中全部修复。

然后:scoop install perry-ts/perry。manifest 钉在 v0.5.345(用 depends: main/llvm 自动拉取官方 MSVC 默认 LLVM)。release 工作流现在会在每个归档旁产出 <artifact>.sha256 sidecar,格式与 sha256sum 兼容,便于任何下游包管理器 bumper 使用。

# Windows host
scoop bucket add perry-ts https://github.com/PerryTS/perry
scoop install perry-ts/perry
perry compile src\main.ts --target windows -o myapp.exe

7. 收尾

本阶段的主线是社区参与加上内部清扫。TheHypnoo 提交了三个重要 PR(#224 perry/updater、#231 fs.appendFileSync 接通、#232 response.arrayBuffer 的 body 字节)。跟踪器清掉了大约 30 个 issue。编译器最大文件缩了 60%,并长出了一个穷尽 walker,让“忘了更新四个临时 walker 中的一个”从运行时错编译变成 cargo build 错误。UI 样式在所有桌面平台上达到等同水准,Windows 上仅剩阴影。Geisterhand 长出了浏览器端的 devtools 表面。Windows 上的安装路径少了一条命令。

试一试:

# npm(任意平台)
npm install @perryts/perry
npx perry compile src/main.ts -o myapp && ./myapp

# Homebrew(macOS)
brew install PerryTS/perry/perry

# Scoop(Windows)
scoop bucket add perry-ts https://github.com/PerryTS/perry
scoop install perry-ts/perry

# 桌面应用自动更新
npm install @perry/updater

# 实时 inspector
perry compile main.ts -o app --enable-geisterhand
./app &  # 然后打开 http://localhost:7676

源码:github.com/PerryTS/perry — Issues:github.com/PerryTS/perry/issues — Changelog:CHANGELOG.md

— Ralph