1. 为什么需要它?
当团队的代码库从几千行膨胀到数十万行,传统的 Python 动态检查往往只能在运行时抛出错误,开发者只能在 CI 或手动运行 mypy 后才发现问题。此时两大痛点凸显:
- 检查延迟:每次完整检查都要遍历全部文件,耗时从几秒飙到几分钟,阻塞编辑器的实时诊断。
- 跨文件依赖不易增量:对一个模块的微小改动会迫使整个项目重新解析,导致 CI 资源浪费。
Pyrefly 把这两个痛点当作“漏水的屋顶”和“水管交叉的水库”。它用 增量图 记录每块代码的水位(类型信息),只在水位变化的管道上补漏,而不是把整栋楼的屋顶都重新铺。核心技术栈围绕 Rust 并发 + union‑find,在保证内存安全的前提下实现“闪电般”的增量检查和完整的 LSP 功能。
关键组件概览:
| 组件 |
作用 |
| pyrefly_types |
Python 类型模型(Type、Union、Class) |
| pyrefly_python |
把源码转成内部 AST |
| pyrefly_graph |
增量依赖图,像水库的闸门记录哪些水流需要重新放水 |
| pyrefly_solver |
变量统一求解器,核心使用 union‑find |
| pyrefly_lsp |
将求解器包装成 LSP,向编辑器提供诊断、补全等实时服务 |
2. 核心架构
系统的“骨架”是一条由 解析 → 构图 → 求解 → 服务 的流水线。可以把它想象成 一个自动化的邮局:
- 投递(解析)把 Python 源码切成信封(AST)。
- 分拣中心(增量图)记录每封信的目的地以及与其他信件的关联。
- 分拣机器人(求解器)依据地址合并相同收件人(类型变量统一),并在必要时把信件转寄(流类型细化)。
- 投递员(LSP)把最终的诊断结果送到开发者的 IDE。
关键类比:union‑find 如城市地下排水系统
想象每个类型变量是城市里的一口井,初始时井之间互不相连。union‑find 就像在地下铺设管道,把两口井连通以共享水位(统一类型)。路径压缩相当于在管道交叉点安装阀门,让后续的水流直接走最短路径,避免重复遍历。正因为这种“管道+阀门”的结构,Pyrefly 能在数十万行代码之间几乎瞬时完成类型统一。
数据流向(一个检查请求的旅程)
- LSP 收到编辑器的“打开文件”事件,触发
pyrefly_lsp 创建一个 **Session**。 Session 调用 pyrefly_python 把文件解析成 AST,并把每个模块的 Symbol 注册进 **pyrefly_graph**。 - 变更导致的节点标记为 脏,图的调度器只把这些脏节点和它们的依赖推送到 **
pyrefly_solver**。 solver 读取 **Variables**(union‑find 实例),在内部的 VariableNode 上执行路径压缩和合并,产生统一的 **Type**。 - 求解完成后,
Session 把诊断信息、补全候选等序列化为 JSON,交给 lsp-server 按 LSP 协议回传给 IDE。
3. 源码级复盘
下面挑选 求解器(solver)、增量图(graph)、LSP 实现(lsp) 三个核心模块,分别从设计哲学、代码拆解、决策分析和潜在风险四个维度展开显微镜式观察。
3.1 pyrefly_solver:核心求解器
功能一句话总结
使用 union‑find 与气体(gas)控制的递归深度,实现跨模块的增量类型约束求解。
设计哲学
求解器的目标是 在保持极致吞吐的同时防止递归失控。Rust 的所有权模型天然防止数据竞争,项目在此基础上额外采用 RefCell 为每个变量提供内部可变性,使得并行路径压缩成为可能。
代码逻辑拆解
// 变量集合:每个 Var 对应一个内部可变的节点
#[derive(Debug, Default)]
struct Variables(SmallMap<Var, RefCell<VariableNode>>);
- 步骤 1:创建映射
SmallMap 按需分配,避免全局锁;RefCell 让求解器在共享 Arc<Variables> 的情况下仍能就地修改节点。
/// 统一两个变量的根节点,返回合并后的根
fn union(&self, a: Var, b: Var) {
let root_a = self.find(a);
let root_b = self.find(b);
if root_a != root_b {
// 路径压缩:把较浅的根直接指向更深的根
self.nodes[root_a].borrow_mut().parent = root_b;
}
}
- 步骤 2:路径压缩
每次 find 都递归抓取父节点并写回,等价于在地下管道里安装短路阀门,让后续水流直接流向最终汇合点。
/// 查找变量的根节点并进行路径压缩
fn find(&self, var: Var) -> Var {
let node = self.nodes[&var].borrow();
if node.parent == var { return var; }
let root = self.find(node.parent);
drop(node); // 释放只读借用
self.nodes[&var].borrow_mut().parent = root; // 写回压缩路径
root
}
- 步骤 3:气体控制递归深度
为防止在极端递归(如深度嵌套的泛型)中栈溢出,求解器在每一次递归入口消耗 gas,耗尽即提前返回错误。
fn solve_type(&self, ty: &Type, gas: &mut Gas) -> Result<Type, SolveError> {
gas.consume()?; // 每进入一次递归消耗 1 单位
match ty {
Type::Union(inner) => self.solve_union(inner, gas),
// …其他分支…
}
}
决策分析
| 方案 |
选用 |
说明 |
Mutex<VariableNode> |
未采用 |
全局互斥锁在高并发求解中会成为瓶颈,导致每个变量的读写都被排队。 |
RwLock<VariableNode> |
未采用 |
读多写少的模型仍需要跨线程同步,频繁的写入(合并)会导致写锁竞争。 |
RefCell<VariableNode> + Arc<Variables> |
采用 |
只在单线程或 Rayon 并行的任务内部使用 RefCell,避免跨线程锁,同时保持安全的共享所有权。 |
gas 递归深度限制 |
采用 |
相比硬性递归深度常量,gas 让求解过程在不同路径上消耗可调,兼顾安全与灵活性。 |
潜在风险
- **跨线程使用
RefCell**:如果未来将求解器迁移到多线程共享 Variables,未加 Sync 的 RefCell 会导致未定义行为。需在改造时引入 parking_lot::RwLock 或 crossbeam::atomic::AtomicCell。 - 气体耗尽:在极端的递归类型(如无限展开的 TypeVar)上,
gas 可能过早触发错误,导致误报。生产环境应将 Gas::new 参数调高或在配置中提供伸缩阈值。
3.2 pyrefly_graph:增量依赖图
功能一句话总结
维护模块之间的类型计算依赖,并在文件改动时精准定位需要重新求解的子图。
设计哲学
增量图的职责类似 “城市的供水调度中心”:每条管线记录哪块水库(模块)供给哪条支线(类型计算),当某座水库泄漏(代码改动)时,只关闭受影响的支线,不必关闭整座城市的供水。
代码逻辑拆解
/// Graph 节点,保存模块的计算结果和依赖集合
pub struct Node {
/// 上游依赖的 Var 集合
pub inputs: FxHashSet<Var>,
/// 下游受影响的 Node 列表
pub dependents: FxHashSet<NodeId>,
/// 缓存的求解结果,使用 Arc 共享避免拷贝
pub cached: Arc<Type>,
}
- 步骤 1:记录依赖
当模块 A 被解析,A 的每个 Var 都会加入 inputs,并把 A 加入被它使用的模块的 dependents。
fn add_edge(&mut self, from: NodeId, to: NodeId, var: Var) {
self.nodes[from].dependents.insert(to);
self.nodes[to].inputs.insert(var);
}
- 步骤 2:脏标记传播
文件修改触发 mark_dirty(node_id),递归向下标记所有受影响的 dependents。
fn mark_dirty(&mut self, id: NodeId) {
if self.nodes[id].dirty { return; }
self.nodes[id].dirty = true;
for &child in &self.nodes[id].dependents {
self.mark_dirty(child);
}
}
- 步骤 3:增量求解调度
调度器遍历 dirty 节点,按拓扑顺序把它们送入求解器。每完成一次求解,cached 被更新,dirty 复位。
fn schedule(&mut self) -> impl Iterator<Item = NodeId> {
self.nodes
.iter()
.enumerate()
.filter(|(_, n)| n.dirty)
.map(|(i, _)| NodeId(i))
}
决策分析
| 方案 |
选用 |
说明 |
手写 Vec<Vec<usize>> 邻接表 |
未采用 |
需要频繁插入/删除,Vec 扩容成本高且不易维护集合语义。 |
FxHashSet(rustc 使用的高速哈希集合) |
采用 |
在大规模模块依赖下提供 O(1) 插入/查询,且内存占用相对较低。 |
Arc<Type> 缓存 |
采用 |
共享只读结果,避免在多线程求解阶段拷贝大型类型结构。 |
RwLock<Node> 保护节点 |
未采用 |
采用单线程调度 + Arc 共享的方式,省去跨线程锁开销,保持增量图的高吞吐。 |
潜在风险
- 图循环检测:如果在
add_edge 时引入循环依赖,调度器的拓扑遍历会死循环。当前实现依赖 mark_dirty 的递归深度,极端情况下可能导致栈溢出。建议在 add_edge 前加入环检测或使用 Kahn 算法生成拓扑序。 - 内存泄漏:
Arc<Type> 若在大量短生命周期的模块之间频繁切换,可能导致旧的 Type 无法及时回收。生产中应监控 Arc::strong_count,必要时对 cached 使用 Weak 引用做惰性回收。
3.3 pyrefly_lsp:语言服务器桥梁
功能一句话总结
把求解器的诊断、补全等结果封装为 LSP 消息,实时推送给编辑器。
设计哲学
LSP 就像 “快递员的调度中心”:所有快递(诊断、补全)先进入中心排队,中心根据线路(文件路径)决定哪位快递员(IDE)先收货。这里的核心是 异步消息队列 与 并发状态管理,确保编辑器在高频键入时仍能得到低延迟响应。
代码逻辑拆解
// LSP 主入口,使用 Tokio 运行时
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let (connection, io_threads) = lsp_server::Connection::stdio();
let mut server = Server::new(connection);
server.run().await?;
io_threads.join()?;
Ok(())
}
- 步骤 1:建立 Tokio 运行时
#[tokio::main] 创建多线程调度器,保证网络 I/O 与内部求解任务可以并行。
impl Server {
async fn run(&mut self) -> Result<(), anyhow::Error> {
while let Some(msg) = self.conn.receiver.next().await {
self.handle_message(msg).await?;
}
Ok(())
}
}
- 步骤 2:消息分派
handle_message 根据 LSP 方法把请求路由到对应的业务模块。对 textDocument/didChange,它会把改动写入 Session,触发增量图的脏标记。
async fn handle_message(&mut self, msg: Message) -> Result<(), anyhow::Error> {
match msg.method.as_str() {
"textDocument/didChange" => self.session.on_change(msg.params).await,
"textDocument/completion" => self.session.on_completion(msg.params).await,
// … 其它协议 …
_ => {}
}
Ok(())
}
- 步骤 3:异步返回
完成求解后,用 self.conn.sender.send(Notification::new(...)) 把诊断结果推送给 IDE,整个过程不阻塞主线程。
决策分析
| 方案 |
选用 |
说明 |
| 同步阻塞 I/O(std::io) |
未采用 |
在高频编辑场景下会导致 IDE 卡顿,无法满足实时反馈需求。 |
async-std 运行时 |
未采用 |
与项目其他异步组件(如 lsp-server)的兼容性不如 tokio,且生态更小。 |
tokio 多线程运行时 |
采用 |
可以在同一个进程内并行处理文件改动、求解任务和网络 I/O,提升整体吞吐。 |
Arc<Mutex<Session>> 共享状态 |
未采用 |
统一的全局锁会在并发请求时产生争用;改用 RwLock + RefCell 的组合在单线程调度下更轻量。 |
潜在风险
- 任务饥饿:如果求解器的 Rayon 线程池被大量长时间的
solve_type 占满,LSP 的网络任务可能被延迟。建议在 tokio 中使用 spawn_blocking
项目地址: https://github.com/facebook/pyrefly