锈分析仪架构

2021-02-06 19:54:24

本文档介绍了rust-analyzer的高级体系结构,如果您想熟悉代码库,就在正确的位置!

请注意,指南和视频都过时了,一般而言,该文档应该更新。

在最高级别上,rust-analyzer是一种从客户端接受输入源代码并生成代码的结构化语义模型的事物。

更具体地说,输入数据由一组测试文件((PathBuf,String)对)和有关项目结构的信息组成,这些信息在所谓的CrateGraph中捕获。crate图表指定哪些文件是crate根,为每个cf标志指定哪些cfg标志板条箱以及板条箱之间存在什么依赖关系。这是输入(接地)状态。分析器将所有这些输入数据保存在内存中,而从不进行任何IO。因为输入数据是源代码,通常最多只能测量几十兆字节,将所有内容保存在内存中就可以了。

"结构化语义模型"基本上是源代码中出现的模块,函数和类型的面向对象的表示形式,这种表示形式是完全解析的:所有表达式都有类型,所有引用都绑定到声明等,这是派生的州。

客户可以提交一小部分输入数据(通常是对单个文件的更改),并获得一个新的代码模型来说明更改。

底层引擎确保模型是按延迟计算的(按需)并且可以快速更新以进行少量修改。

本节简要讨论各种重要的目录和数据结构,请注意``体系结构不变式''部分,它们经常讨论源代码中故意缺少的内容。

这是rust-analyzer的构建系统。我们使用货物来编译rust代码,但是还有其他各种任务,例如发布管理或本地安装。它们由Rust代码在xtask目录。

这是一个手写的递归下降解析器,它会产生一系列事件,例如"起始节点X&#34 ;、"结束节点Y&#34 ;,其工作方式与kotlin的解析器相似,这是处理语法错误和输入不完整的很好的灵感来源。原始的libsyntax解析器是我们用于Rust语言定义的工具。 TreeSink和TokenSource特性在语法和罗文树之间架起了不可知树的语法分析器。

架构不变:解析器独立于特定的树结构和令牌的特定表示形式,它将事件的一个统一流转换为事件的另一个统一流。令牌独立性使我们既可以解析基于文本的源代码,也可以解析基于tt的事件宏输入。树独立性使我们可以更轻松地更改语法树的实现方式,它还应解锁高效的光解析方法,例如,您可以提取文件中定义的名称集(用于错字校正)而无需构建语法树。

语法的ungrammar描述,用于使用cargo xtask codegen命令生成syntax_kinds和ast模块。

ra_syntax的测试主要是数据驱动的。 test_data / parser包含具有一堆.rs(测试向量)和.txt文件的子目录以及具有相应语法树的.txt文件。在测试过程中,我们针对.txt检查.rs。如果缺少.txt文件,则会创建该文件(这就是方法此外,运行cargo xtask codegen将遍历语法模块,并将所有// test test_name注释收集到test_data / parser / inline目录内的文件中。

添加新的内联测试后,您需要运行cargo xtest代码生成并如上所述更新测试数据。

体系结构不变:语法箱完全独立于rust-analyzer的其余部分。它对salsa或LSP一无所知。这很重要,因为可以仅使用语法树来制作有用的工具,而无需语义信息,则无需构建代码,这会使工具更强大。另请参阅https://web.stanford.edu/~mlfbrown/paper.pdf。您可以查看语法箱作为rust-analyzer的入口点。语法箱是API边界。

体系结构不变性:语法树是一种值类型,该树完全由其语法节点的内容确定,它不需要全局上下文(例如内部语言),也不存储语义信息。因为语义信息存储在传统编译器中很方便,但是在IDE中不能很好地工作。特别是,辅助和重构需要转换语法树,如果您需要对语义信息进行某些操作,这将变得很尴尬。

体系结构不变性:语法树是为单个文件构建的,用于对所有文件进行并行解析。

体系结构不变性:语法树在设计上是不完整的,不会强制格式正确。如果AST方法返回一个Option,则即使在语法上禁止,在运行时它也可以是None。

我们将salsa板条箱用于增量和按需计算。大致上,您可以将salsa视为键值存储,但是它也可以使用指定的函数计算派生值。 base_db板条箱提供了与salsa交互的基本基础结构。至关重要的是,它定义了大部分" input"查询:分析器的客户端提供的事实。读取base_db :: input模块的文档应该是有用的:其他所有内容都严格从这些输入中得出。

体系结构不变性:构建系统的特殊性不是基础状态的一部分,特别是base_db对货物一无所知.CrateGraph结构用于抽象地表示包装箱之间的依赖关系。

架构不变式:base_db不了解文件系统和文件路径。文件用不透明的FileId表示,没有任何操作可以从FileId中获取std :: path :: Path。

hir_xxx板条箱具有很强的ECS风格,因为它们可以处理原始ID并直接查询数据库。此处的抽象很少,这些板条箱与莎莎和粉笔融为一体。

名称解析,宏扩展和类型推断都在这里发生。这些板条箱还定义了核心的各种中间表示形式。

ItemTree将单个SyntaxTree压缩为&summary"数据结构,对功能体进行修改后保持稳定。

架构不变式:这些包装箱明确地关心增量。我们维护的核心不变式是在函数体内键入,永远不会使全局派生数据无效。即,如果您更改foo的主体,则所有关于bar的事实应保持不变。

架构不变式:hir仅存在于带有特定CFG标志的特定板条实例的上下文中。如果板条箱多次参与板条图,则相同的语法可能会产生多个HIR实例。

顶层租用箱是API边界。如果您考虑使用rust-analyzer作为库,那么租用箱最有可能是您要与之交谈的。

它将ECS样式的内部API包装为更具OO风格的API(每个调用都带有一个额外的db参数)。

体系结构不变性:hir提供了一个静态的,完全解析的代码视图。虽然内部hir_ *条板箱用于计算事物,但hir从外部看起来像是一种惰性数据结构。

hir还处理从语法到对应的hir的艰巨任务,请记住此处的映射是一对多的。请参阅语义类型和source_to_def模块。

特别要注意source_to_def中的一个奇怪的递归结构,我们首先将父语法节点解析为父hir元素,然后询问hir父节点有哪些语法子元素,然后在子元素集中寻找我们的节点。

这是很多IDE功能的核心,例如goto定义,首先要确定光标处的hir节点,这是某种(至今未命名)的uber-IDE模式,它也存在于Roslyn和Kotlin中。

ide箱基于hir语义模型构建,可提供诸如完成或goto定义之类的高级IDE功能,它是一个API边界。如果您想通过LSP,基于自定义的平面缓冲区的协议使用rust-analyzer的IDE部分作为文本编辑器中的库,这是正确的API。

架构不变性:ide crate的API是基于具有公共字段的POD类型构建的,该API使用编辑器的术语,它谈论的是偏移量和字符串标签,而不是定义或类型方面的内容。 MVC中的view和MVVM中的viewmodel。所有参数和返回类型在概念上都是可序列化的,特别是API中通常不包含语法tress和hir类型(但在实现中大量使用)。向LSP开发人员大声疾呼以普及认为" UI"是划定界限的好地方。

ide也是第一个具有随时间变化的概念的板条箱。 AnalysisHost是一种状态,您可以在其中以事务方式应用apply_change。分析是状态的不变快照。

在内部,ide分为多个板条箱。 ide_assists,ide_completion和ide_ssr实现了大型隔离功能。 ide_db实现了常见的IDE功能(特别是在此处实现了参考搜索)。ide包含一个公共API /外观,以及许多较小功能的实现。

架构不变性:ide crate努力提供一个完美的API。尽管目前只有一个使用方,即LSP服务器,但LSP并不会影响LSP的API设计,相反,我们牢记一个理想的理想客户端-一个专门为生锈量身定制的IDE,其中的每个角落都带有Rust特有的东西。

这个板条箱定义了rust-analyzer二进制文件,因此它是切入点。它实现了语言服务器。

体系结构不变性:rust-analyzer是唯一了解LSP和JSON序列化的板条箱。如果要将数据结构X从ide公开到LSP,请不要使其可序列化,而是在rust-analyzer中创建可序列化的副本条板箱并在两者之间手动转换。

GlobalState是服务器的状态。main_loop定义了接收请求并发送响应的服务器事件循环。修改状态或可能阻止用户键入的请求在主线程上处理,所有其他请求在后台处理。

体系结构不变:服务器是无状态HTTP HTTP,有时需要在请求之间保留状态,例如"最后一次完成编辑的第五个完成项目的编辑内容是什么?为此,第二个请求应包括足够的信息以从头开始重新创建上下文。这通常意味着包括原始请求的所有参数。

reload模块包含处理配置和Cargo.toml更改的代码。这是一项棘手的工作。

不变的体系结构:即使构建被破坏,rust-analyzer也应该部分可用。重新加载过程不应阻止IDE功能正常工作。

这些板条箱用于处理货物,以了解项目结构并获取编译器错误以进行“保存时检查”。特征。

他们大量使用板条箱/路径而不是std :: path。单个rust-analyzer进程可以为许多项目提供服务,因此重要的是服务器的当前目录不要泄漏。

这些板条箱将宏实现为令牌树->令牌树转换,它们独立于其余代码。

这些板条箱实现了一个虚拟文件系统,提供了基础文件系统的一致快照并隔离了混乱的OS路径。

架构不变:vfs不会假设使用单个统一文件系统,即,一个rust-analyzer进程可以充当两台不同机器的远程服务器,其中相同的/tmp/foo.rs路径指向不同的文件。因此,所有路径API通常都采用某个现有路径作为文件系统见证人。

这个箱子包含各种非锈分析仪特定的工具,这些工具可能已经存在于std中,以及我们想利用的不稳定std物品的副本,例如std :: str :: split_once。

该存储库的某些组件是通过自动过程生成的。 cargo xtask codegen运行所有生成任务。生成的代码通常提交给git存储库。有一些测试可以检查生成的代码是否新鲜。

不变的体系结构:我们避免引导,对于代码生成我们需要解析Rust代码,使用rust-analyzer既可以工作,也很有趣,但是也会使构建过程变得非常复杂,因此我们使用了syn和manual字符串解析。

假设当用户键入foo时,IDE正在计算语法高亮显示,应该怎么办? rust-analyzers的答案是应取消突出显示过程-现在的结果是陈旧的,并且它还阻止修改输入。

salsa数据库维护一个全局修订计数器,当应用更改时,salsa碰触该计数器并等待所有其他使用salsa的线程完成。 (请参阅Canceled :: throw)。也就是说,生锈分析仪需要展开。

ide是捕捉恐慌并将其转换为结果< T,已取消>的边界。

最外面的边界是rust-analyzer板条箱,它根据stdio定义了LSP接口。我们对该组件进行集成测试,方法是向其提供LSP请求流并检查响应。这些测试称为"。繁重,因为它们与Cargo交互并从磁盘读取真实文件。因此,我们尝试避免在此边界上编写过多的测试:以静态类型的语言,很难在如果消息本身是键入的,则为协议本身。仅在设置RUN_SLOW_TESTS env var时运行大量测试。

中间且最重要的边界是ide。与暴露API的rust-analyzer不同,ide使用Rust API且旨在供各种工具使用。典型测试创建了AnalysisHost,调用了一些Analysis函数并将结果与​​预期进行比较。

最内层和最复杂的边界是hir。它具有比ide更丰富的类型词汇,但是基本的测试设置是相同的:我们创建一个数据库,运行一些查询,声明结果。

为了测试各种分析极端情况并避免忘记旧的测试,我们使用了所谓的标记。有关更多信息,请参见test_utils板条箱中的marks模块。

体系结构不变:rust-analyzer测试不使用libcore或libstd。所有必需的库代码必须是测试的一部分,从而确保快速执行测试。

架构不变:测试是数据驱动的,不测试API。直接调用各种AP​​I函数的测试是一个责任,因为它们使API的重构变得更加复杂,因此大多数测试如下所示:

fn check(input:& str,Expect:Expect_test :: Expect){//实际执行特定API的单个位置}#[test] fn foo(){check(" foo&#34 ;,期望![[" bar"]]);}#[test] fn spam(){检查(" spam&#34 ;,期待![[" eggs"]] );} // ... ...还有一百多个根本不关心特定API的测试。

为了指定输入数据,我们使用特殊格式的单个字符串文字,它可以描述一组rust文件。请参见Fixture类型。

体系结构不变式:所有代码不变式均通过#[test]测试进行测试。CI中没有其他检查,格式和整洁测试均与货物测试一起运行。

体系结构不变:测试不依赖任何外部资源,它们是完全可重复的。

体系结构不变性:锈分析器(ide / hir)的核心部分不会与外界交互,因此不会失败,仅允许接触LSP的部分进行IO。

rust-analyzer的内部需要处理破损的代码,但这不是错误条件。rust-analyzer健壮:各种分析计算(T,Vec< Error)而不是Result< T,Error>。

rust-analyzer是一个复杂的长期运行的过程,它始终会存在错误和崩溃,但是在孤立的功能中发生的崩溃不应降低整个过程。每个LSP请求都受catch_unwind保护。我们始终使用且从不使用宏而不是断言要从不可能的情况中恢复。

rust-analyzer是一个运行时间很长的过程,因此了解内部情况很重要。为此,我们有几种仪器。

运行rust-analyzer的事件循环非常明确,它不是生成期货或安排回调(打开),而是接受可能的事件枚举(关闭),很容易看到所有触发生锈的事物分析仪处理及其性能

rust-analyzer包含一个简单的分层探查器(hprof),可通过RA_PROFILE =' *> 50 env var(记录所有(*)耗时超过50毫秒的动作)启用,并产生如下输出:

85ms-handle_completion 68ms-import_on_the_fly 67ms-import_assets :: search_for_relative_paths 0ms-crate_def_map:wait(804次调用)0ms-find_path(16次调用)2ms-find_like_imports(1次调用)0ms -generic_params_query(334次调用)59ms-trait_solve_query -语义:: analyze_impl(1个调用)1毫秒-渲染分辨率(8个调用)0ms-语义:: analyze_impl(5个调用) 同样,我们保存了活动对象计数(RA_COUNT = 1)。它不足以在prod中启用,这是一个错误,应该修复。