浅谈 language server & LSIF & SARIF & Babelfish & Semantic & Tree-sitter & Kythe & Glean等

注:本人做过静态代码分析,也算是做过code intelligence,两者互有交叉,所以这里将涉及到的有意思的东西记录下来,未来有精力再依次对这些项目进行介绍

什么是language server protocol

注:这里复述我之前的总结

LSP(Language Server Protocol)是微软在开发visual studio code中针对Language Server设计的一种协议,关于设计中的抉择,在文章《Language Server Extension Guide》有详细介绍。

下面我粘贴了这篇文章中的一些描述,采用Language Server的原因主要分为如下三点:

  • 不同语言的language server在实现时,通常采用它们自身的语言,例如C++的language server Clangd就是C++实现的。而VSCode只自带了Node.JS的运行时。
  • language server通常是资源密集型的,一言不合就要重新编译整个源码文件,所以就有将langauge server运行放在远端的需求
  • 最后从 M(编辑器) * N(language server实现) 转化成了 M(编辑器) + N(language server实现)

注:由于language server的是编译器的子集,简单的例如符号跳转,diagnostics等功能,编译前端就可以实现,但是另外的一些功能,例如analysis,则需要编译器偏后端的部分实现。由于编译器通常是native programming language实现的,所以language server的实现也会基于现有的编译器实现

First, Language Servers are usually implemented in their native programming languages, and that presents a challenge in integrating them with VS Code which has a Node.js runtime.

Additionally, language features can be resource intensive. For example, to correctly validate a file, Language Server needs to parse a large amount of files, build up Abstract Syntax Trees for them and perform static program analysis. Those operations could incur significant CPU and memory usage and we need to ensure that VS Code’s performance remains unaffected.

Finally, integrating multiple language toolings with multiple code editors could involve significant effort. From language toolings’ perspective, they need to adapt to code editors with different APIs. From code editors’ perspective, they cannot expect any uniform API from language toolings. This makes implementing language support for M languages in N code editors the work of M * N.

个人认为 language server protocol 或者类似的概念在云时代会越来越重要,未来的程序员的开发会越来越摆脱本地的限制,从开发到部署都会在云上实现,这是不可扭转的趋势,而 language server protocol 或者类似的概念会在其中扮演越来越重要的作用。

JSONRPC

注:这里复述我之前的总结

由于LSP是基于JSONRPC2实现的,所以JSONRPC的概念是language server中非常重要的一环。由于自己不是后端工程师,对网络不是很熟,所以这里没有什么经验可谈。只是知道了remote procedure call这种抽象后,自己以前对什么是程序以及程序的执行理解的太肤浅了(这就是学习没有举一反三的原因,没有抽象出本质,callercallee只是一种约定,完成约定就行,即使不同编程语言也OK)

RPC的核心就是以下三点:

  • Call ID的映射,如何唯一确定callee的ID
  • 序列化和反序列化,由于参数等信息不是通过栈(或者本机内存)传递,所以就需要转换到传输介质(例如网络)所要求的格式进行传递
  • 网络传输,RPC中的R-remote决定了procedure call需要跨越网络

注:知乎用户用心阁的回答和文章Remote Procedure Calls对我的帮助比较大。

LSIF(Language Server Index Format)

LSIF是一种描述程序信息的格式,目前主要用于code intelligence的领域,例如Code intelligence with LSIFRich code navigation

The purpose of the Language Index Format (LSIF) is it to define a standard format for language servers or other programming tools to dump their knowledge about a workspace. This dump can later be used to answer language server LSP requests for the same workspace without running the language server itself. - Language Server Index Format

LSIF确定了从程序中提取出来的程序信息的格式,这个所谓的格式有很多:

  • AST也算是一种表示程序的格式,但AST有下面两个问题
    - 编程语言相关,保留的语言细节太多
    - 比较原始,不能为programming tools直接提供信息
    - 虽然有通用的AST,例如Babelfish,但AST还不是一个好的选择
  • SARIF(Static Analysis Results Interchange Format),但这只是静态代码分析工具之间交互的格式,太过于针对特定领域
  • Semmle(我不太确定Semmle的格式),Semmle应该是将AST+call graph + cfg等存到数据库中,不够通用。
  • Google的Kythe也定义了类似的schema来抽象出程序的内部结构。
    注:Kythe的发起者是牛人Steve Yegge,也是就会王垠在google工作时的上司,给了王垠比较高的评价 😃
  • Glean也定义了与Kythe和Babelfish类似的format来存储数据

感觉未来“程序也是数据”的观点会越来越重要,LSIF可以看做一种精简的,比较靠前的,用于answer programming tools的数据格式。

sourcegraph(也就是王垠第一个吐槽的公司 😃)采用了LSIF来存储程序数据来驱动code intelligence的能力。

What format is that code intelligence data in? We are using LSIF (Language Server Index Format) which is a graph of code intelligence information such as definitions, references, hover documentation, similar in spirit to Kythe. The graph is comrised of vertices for each definition/reference/hover and edges that connect references to definitions. - Code intelligence with LSIF

现在提供 code intelligence 的通用做法是通过 long lived language server 来不断地 answer frontend发过来的request。既占用资源也不稳定,如果我们能够将程序数据提前使用LSIF保存下来,那么就可以缓解language server对资源的占用,例如不需要从AST中解析出想要的数据。

例如对于下面的代码来说,

function bar() {
}

function foo() {
  bar();
}

我们可以使用下面的图形来保存程序的信息。
LSIF
用JSON表示出来就是下面的形式:

// The document
{ id: 4, type: "vertex", label: "document", uri: "file:///Users/dirkb/sample.ts", languageId: "typescript" }

// The bar declaration
{ id: 6, type: "vertex", label: "resultSet" }
{ id: 9, type: "vertex", label: "range", start: { line: 0, character: 9 }, end: { line: 0, character: 12 } }
{ id: 10, type: "edge", label: "next", outV: 9, inV: 6 }

// The bar reference range
{ id: 20, type: "vertex", label: "range", start: { line: 4, character: 2 }, end: { line: 4, character: 5 } }
{ id: 21, type: "edge", label: "next", outV: 20, inV: 6 }

// The reference result
{ id : 25, type: "vertex", label: "referenceResult" }
// Link it to the result set
{ id : 26, type: "edge", label: "textDocument/references",  outV: 6, inV: 25 }

// Add the bar definition as a reference to the reference result
{ id: 27, type: "edge", label: "item", outV: 25, inVs: [9], document: 4, property : "definitions" }

// Add the bar reference as a reference to the reference result
{ id: 28, type: "edge", label: "item", outV: 25, inVs: [20], document:4, property: "references" }

SARIF(Static Analysis Results Interchange Format)

SARIF几乎可以说是未来静态分析工具都会采用的数据格式,SARIF囊括了静态分析的方方面面,例如call grah,CFG,constraint信息,symbolic value,模拟的region,缺陷等级,缺陷位置,缺陷路径等等。可以说比LSIF复杂多了,虽然复杂但静态分析工具可以有选择的支持。SARIF主要是由静态分析厂商Semmle和微软主导的,很多厂商都参与到其中,虽然SARIF的起草和指定还是由微软,Semmle和GrammarTech等完成的。
member
注:上图来源于SARIF: Interoperability standard for static analysis tools

注:上图还有我的老东家360

已经有一些工具开始支持SARIF了,

如下图所示,我们可以看到SARIF的一个主要作用就是提供一种让不同programming tools通信的数据格式。另外,没有一种静态分析工具或者方法能够覆盖所有场景,所以一般的策略是通过工具hybrid的方式来完成,此时SARIF就是不可或缺的“通信格式”。GrammarTech对SARIF所能解决的问题进行了总结:

  • An IDE such as Eclipse, or a code editor such as VS Code. Users like to see static analysis results overlaid on their normal code views.
  • A code-review tool such as Phabricator, or the github review subsystem. A static analysis tool might be set up to populate the review with comments on the diff.
  • A results analytics tool such as SonarQube that needs to incorporate information from several static analysis tools into a dashboard or report.
  • A bug-tracking system such as Jira or Bugzilla. A user might want to ask if a reported defect was detected in the most recent analysis.
  • A continuous integration system such as Jenkins. The results from the static analysis tool can be used to indicate the status of the build. E.g., any “severe” security findings could cause the build to be considered “failed”.

SARIF

注:上图来源于Static Analysis Results: A Format and a Protocol: SARIF & SASP

SARIF未来能够打通以下几类工具之间的界限,想想还有些小激动呢。

  1. editor,例如Eclipse,VS Code等;
  2. code review,例如Phabricator,gitlab,github等;
  3. bug追踪工具,例如Jira,Bugzilla等;
  4. CI工具,Jenkins,Travis CI等;
  5. 静态分析工具,sonarcube,Infer和coverity等;

注:鉴于微软收购了github,而github已经收购了semmle tech,而且github推出了github actions,所以github除了除了缺陷追踪工具之外,集成了上述的四部分 😃

SASP(Static Analysis Server Protocol)

类似于LSP,既然SARIF提供了静态分析工具输出数据的结果格式,那么就需要定义它们之间的通信协议。而这个协议叫做SASP,但是这个东西现在太fancy了,还没有确定下来,只是暂时有这个东西。Grammar Tech给除了一个SASP适用的场景:

To illustrate how SASP would work, consider the use case where a Github adapter needs to communicate with the results manager to populate a review of a pull request with comments.

  1. The adapter forms a query to the results manager to ask which analyses have results that are relevant to code changed by the pull request.
  2. The results manager authenticates the request (results managers must be secure), and responds with the set of results. These will typically be filtered by excluding the following:
    - Uninteresting results (e.g., those with a score less than a threshold).
    - Results that are annotated by users as false positives or “don’t care”.
    - Results that the user is not authorized to see (again, security is important).
    : 3. The analysis results are delivered to the adapter as a SARIF document augmented with metadata about the query that was used to produce them.
  3. The adapter creates the Github review comments and adds them to the pull request thread
  4. The adapter sends a reference to each comment to the results manager, so that users who view the result in contexts outside of Github can see that it is associated with the review.

注:上述内容来源于Static Analysis Results: A Format and a Protocol: SARIF & SASP

GrammarTech采用GraphQL?(未来补充)

既然 SASPLSP 都是programming tools相关的协议,自然就有人尝试将两者结合起来,见Rich diagnostics extension #502,但好像没什么人响应。

Semmle QL

前几个月Semmle(Semmle的开发人员在PLDI上一直有参与)被github收购了,感觉巨硬家在服务程序员用户体验方面做的越来越好了,Semmle有两点比较出名,一是LGTM,一个是Semmle QL。
注:下面的内容摘自PLDI 2018上的一个session《 Declarative Program Analysis with QL》,但是slides的链接找不到了

Semmle采用的分析路径是:

  • Code is data。将程序数据存储到关系型数据中。
  • Analyses are queries。使用Semmle QL查询数据库来进行分析。
  • Query results are alerts。query到的结果就是缺陷信息,这个信息可以集成到lgtm.com中或者IDE中。

整个的分析过程如下如所示,整个过程可以划分为三个模块:

  • 使用现有的编译前端提取程序信息(AST,types,call graph,cfg?),然后存储到关系型数据库中
  • Query Compilation,将quey用的Semmle QL进行编译
  • 执行Query编译后的结果。

注:其实整个过程类似于SQL,只是这里的数据是code data,SQL是Semmle QL

semmle

由于我对Datalog不熟悉,通过slides还是比较难一窥细节,下面是query evaluation步骤的说明:
query

下面是Semmle QL的feature,
在这里插入图片描述

Semmle QL的实现,还是挺活跃的。Semmle QL可能最早追溯到2007年的论文Keynote Address: .QL for Source Code Analysis。我没有用过Semmle QL但是看slides中的描述,这个工具还是挺全面的,能够覆盖实际代码中的很多种场景。

真是慨叹国外开发人员打磨产品细节的能力和决心,Semmle应该是脱胎于牛津大学,经过10多年的不断的打磨,最终做成了现在的样子,希望习惯了在被人屁股后面亦步亦趋的国内开发人员也能够早日独立做出像样的产品 😦

QL: Object-oriented Queries on Relational Data待看论文

Babelfish(注:source{d}已经凉凉,网址已然打不开了

Babelfishsource{d}的工具,source{d}的口号是“The Data Platform for the Software Development Life Cycle”,可以说这个公司基本上是用来“监视”开发人员的,在国内应该比较有市场 😃。

Basbelfish is a self-hosted server for source code parsing. The Babelfish service can parse any file, in any supported language, extracting an Abstract Syntax Tree (AST) from it and converting it into Universal Abstract Syntax Tree (UAST). The UAST enables further analysis and transformations with either the included tools or your own tools by providing a standard open format. - Babelfish - Universal Code Parser

Babelfish发源于如何在code上做machine learning,见Santiago M. Mola - Babelfish: Universal Code Parsing Server他们的技术途径如下:

  1. 2015年,Shallow analysis based on Git history
    - fetch every Git repository
    - Apply PageRank to contributors
    - Measure contributed bytes per language
  2. 2016年,使用了一个编译前端的计数
    - 使用Regexps过滤注释
    - 使用Regexps检测一些库
    - 使用Tokeninzing和pattern mathcing来提取更多的信息
    - 基于Go AST写了一个extractor来提取Go函数和Block中的token

所以我们可以看到Babelfish是想在程序代码上做machine learning,而machine learning第一点需要考虑的是数据是什么,以及该如何存储数据,这也是为什么Babelfish的技术路线从characters -> 结构化的UAST。因为程序来说,最容易表示易处理而且带有语义信息的数据是AST,有了AST才能“理解”程序代码。Babelfish和Kythe以及LSIF不同的地方在于,前者是想在代码上做机器学习,而后者是基于程序代码做code intelligence。机器学习有其感兴趣的代码属性,这个属性可能从AST上并不能直接体现出来,所以提出了UAST的概念去表示所有机器学习感兴趣的属性信息,例如UAST剥离了程序代码中的类型信息。

如下图所示,下面是一段代码的UAST展示。通过一种统一的UAST格式,
babelfish
注:可以访问http://play.bblf.sh/

babelfish极其贴心的对比了一些技术方案解释为什么最终采用了UAST的方式,还是有参考意义的。另外Google中Kythe的开发人员离职后加入了sorce{d},或者这可以解释UAST与Kythe比较接近。

Semantic

Semantic是github一种Haskell大牛写的语言通用的code intelligence工具,我还没有研究它的具体实现,所以这里留白,只贴一下Semantic的一个doc

How we parse source code into ASTs

Semantic也要面临同时parse多种编程语言以及如何以统一的方式表示多种编程语言的问题。解释了很大一通为什么选择tree-sitter,其实一句话就可以概括“因为tree-sitter是github内部开发的工具,semantic也是内部开发的工具,semantic就应该基于tree-sitter实现”。

Kythe

source{d}工程师Michael(前Kythe主力开发)的视频The Road to Semantic Indexing: An introduction to the Kythe project and schema是一个非常好的入门视频。对项目进行Index是非常重要的一个步骤,特别是对Google来说,有非常多的项目,代码量巨大,我们需要对这些代码的一些信息进行一些基本的掌握,例如有多少函数?有多少类型?它们之间有什么联系?我们可不可以给它们赋一个唯一的编码?Index是code search的基础,也是在其上做machine learning的基础。下面的两个截图说明了Kythe项目创立的初衷。

kythe1
kythe2
kythe3
说实话我已经厌倦了M+N优于M*N的说法,从LLVM,到LSP到TVM,现在越来越多的项目想要抽象出来毫无必要的一层。

类似于Babelfish中的UAST,Kythe的目的也是抽象出来一层通用的结构来弥合不同语言的AST。下图展示了如何使用Kythe做Jump-to-Definition,这个图是AST的简化,具体场景决定了需要采用不同的技术路线。这样的一种图示,可以过滤掉AST琐碎的细节,并“预先”建立好了不同节点的关系。
kythe4
关于Kythe schema的文档细节参见,Kythe Schema Reference

相关资料:
Luke Zarko - Scalable cross-references across languages

tree-sitter

tree-sitter算是另外一个github的明星项目,github毕竟是代码管理的,守着这么大的开源代码库,不做点代码相关的工具肯定比较可惜。现在tree-sitter在github自家的semantic和atom中使用了,虽然我感觉在semantic中使用tree-sitter理由不太充分。

下面tree-sitter的描述摘自官网:

Tree-sitter is a parser generator tool and incremental parsing library. It can build a concrete syntax tree for a source file and efficiently update the syntax tree as the source file is edited. Tree-sitter aims to be:

  • General enough to parse any programming language
  • Fast enough to parse on every keystroke in a text editor
  • Robust enough to provide useful results even in the presence of syntax errors
  • Dependency-free so that the runtime library (which is written in pure C) can be embedded in any application

这里我们可以看到tree-sitter侧重的是 parser generatorincremental parsing 。对于第二点,tree-sitter与Anders Hejlsberg 2016年Channel 9的一个talk《Anders Hejlsberg on Modern Compiler Construction》有异曲同工之妙,随着计算机性能的提升,compiler的技术越来越多的被用到IDE工具中,各种code intelligence工具涌现,使得程序员借助compiler来更好的编写程序成为可能,Anders Hejlsberg认为我们需要高效地以增量的方式去维护一个AST,并且对error有很大的容忍度,因为IDE内嵌的compiler前端面对的是一个broken program。所以这是为什么现在的compiler原来越多地放出来compiler API,来帮助programming tools,像是clang,像是Golang。如下图所示,假设foo.处在红色方框位置的时候,你所需要维护的只是从红色方框到AST根节点的path,其它绝大部分的AST你都可以复用。这也成为了tree-sitter实现的基础。

在这里插入图片描述
相关视频:TREE-SITTER - A NEW PARSING SYSTEM FOR PROGRAMMING TOOLS

可以说tree-sitter新颖的地方在于,对于在现有parser的理论上进行微调,以应对现实场景中incremental parsing的需求。

parser相关补充

parser的理论研究几十年前就已经很成熟了,所以现在都是使用场景上的革新,例如利用parser做code intelligence等。parser的计数分为两大流派,top-down和bottom-up。top-down就是LL(k)系列,bottom-up就是LR系列。

LL(Left-to-right, Leftmost derivation)

LL parser通过先前看token的方式,去选择要应用的文法产生式。例如遇到在parse的时候,向前看了一个token while,说明我们要应用的文法一定是while-statement -> ...。所以需要一个预测表来基于向前看的token进行推导,构建预测表需要先计算first集合和follow集合,例如Generate First Set, Follow Set, and Predict Set from Grammar.pdf。first集合一般好理解,那么为什么需要follow集合呢?如下文法所示,当我们向前看到b时,没有first集合能够应用到b上,此时就需要follow集合,因为B->εB的follow集合含有b,此时我们就可以继续应用A -> aBb可。LL通过向前看,驱动整个parse过程

 A -> aBb
 B -> c | ε
// And suppose the input string is “ab” to parse. 

相关资料:
Why FIRST and FOLLOW in Compiler Design?
Construction of LL(1) Parsing Table

LR(Left-to-right, Rightmost derivation in reverse)

LR的核心在于 shift(移入) + reduce(归约),所以需要一个栈来保存部分的等待归约的终结符。如果遇到了shift和reduce的冲突,此时就需要向前看,来解决重冲突。典型的架构如下图所示:
在这里插入图片描述
注:上图来源于LR parser

总结一下两者的区别,

LL ParserLR Parser
First L of LL is for left to right and second L is for leftmost derivation.L of LR is for left to right and R is for rightmost derivation.
It follows the left most derivation.It follows reverse of right most derivation.
Using LL parser parser tree is constructed in top down manner.Parser tree is constructed in bottom up manner.
In LL parser, non-terminals are expanded.In LR parser, terminals are compressed.
Starts with the start symbol(S).Ends with start symbol(S).
Ends when stack used becomes empty.Starts with an empty stack.
Pre-order traversal of the parse tree.Post-order traversal of the parser tree.
Terminal is read after popping out of stack.Terminal is read before pushing into the stack.
It may use backtracking or dynamic programming.It uses dynamic programming.
LL is easier to write.LR is difficult to write.
Example: LL(0), LL(1)Example: LR(0), SLR(1), LALR(1), CLR(1)

注:上表来源于Difference between LL and LR parser

Glean

其实我对Glean不是很了解,只是知道有这样一个东西,Simon Marlow - Glean - facts about code,所以这里暂时列了上来。这个视频的talker是Simon Marlow,也是编程语言大佬,GHC的作者。

Our system for collecting, deriving, storing, and querying facts about code. - Glean

Glean的介绍还是很粗线条的,肯定还有很多细节问题需要解决。而且就像reddit上有人评价,这个东西和Kythe很像,也和source{d}做的东西很相似,只是侧重点上略有不同。

Glean1
Glean2

相关资料:
Simon Marlow - Glean - facts about code

comparison with LSIF

LSIF provides you with the tools to implement what the language server protocol does, but offline.

Glean seems to be focused on having a broad vocabulary for analyses. Points-to analysis, control flow, whatever kind of facts you can encode as datalog rules.

This strikes me as a broader mission than an inert json format for persisting fixed IDE functionality.

Responsive compilers

这个视频我还没仔细看,它大致描述了现代的compiler不应该再作为一个被动的工具,而是发挥它的功能,以主动的方式参与到程序员的编码过程中。以为compiler各个阶段得到的数据都非常有价值,我们需要以某种特定的方式将这些数据存储到DB中。角度很新颖,但技术上没有特别的革新。

总结

随着互联网的崛起,未来会有越来越多的代码数据,大数据时代在十年前就已经到来了,随之而来的机器学习和深度学习也得到了蓬勃发展。云时代的来临,这方面也会有很多创业机会,都是企业软件领域。但是在庞大的代码数据面前,深度学习和机器学习技术还没有得到广泛的应用,个人感觉有以下原因:

  • 程序中有哪些有价值的数据?像是source{d}尝试从整个程序开发生命周期中挖掘数据
  • 传统的场景,例如缺陷检测方面,深度学习技术还没能产生对传统静态分析工具的优势(这方面我不太懂,不敢乱说)
  • 如何从程序中挖掘有价值的数据,不仅需要数据科学家,更多需要编译方面的人才

未来待学习

GraphQL
Datalog,Semmle和Glean都用到了
研读论文 Keynote Address: .QL for Source Code Analysis 并试用Semmle QL
研读论文 QL: Object-oriented Queries on Relational Data
Thrift
Responsive compilers

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值