Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
imbant committed Jan 13, 2025
1 parent f949a8e commit 09c6d37
Showing 1 changed file with 45 additions and 16 deletions.
61 changes: 45 additions & 16 deletions source/_posts/LSP2.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ tags: [LSP, VS Code, 语言服务器]
先回顾一下编译原理的基本流程:
`词法分析` -> `语法分析` -> `语义分析` -> `中间代码生成` -> `代码优化` -> `目标代码生成` -> `...`

## 前端相似
### 前端相似

具体各流程的作用就不赘述了,大学课本里那些长篇大论介绍 LL(1) 文法过于乏味。
我们基本可以把编译器的工作分为`前端`、(`中端`)、`后端`几个流程,先说结论,编译器和语言服务器在编译的过程中,`前端`的过程是非常相似的。也就是 `词法分析``语法分析``语义分析` 几个阶段。
Expand All @@ -24,38 +24,67 @@ tags: [LSP, VS Code, 语言服务器]

换句话说,对于 `console.log` 这一行代码,两者都经历了这样的阶段:

```
IDENTIFIER DOT IDENTIFIER // 词法分析,得知输入是两个符号中间有个点号
```
| 阶段 | 进度 |
| --- | --- |
|源码|`console.log`|
| 词法分析|`IDENTIFIER` `DOT` `IDENTIFIER`|
| 语法分析|规约为 `DomainDotAccess`,是通过点号访问域的语法|
| 语义分析|`console` 符号是一个接口,`log` 符号是其中一个方法|

```
DomainDotAccess // 语法分析,合并 token 得知这是通过点号访问域的语法
```

最终在语义上,就知道 `console` 这个符号的语义是接口 (interface),`log` 符号是其中的一个方法 (method)
这里的 `DomainDotAccess` 是个示意,实际上可能是 `MemberExpression` 或者 `PropertyAccessExpression` 之类的,只是一个命名问题。

### 容错

此外,两者的容错处理也是不一样的。当源码中出现了错误:
编译器会停止编译,通过标准输出等方式抛出`编译错误`,自然也没有目标产物的输出了,当然,一行代码的改动可能引起数个文件的错误,如果只遇到一个错误就停止编译,也不利于 debug,往往会尝试尽可能多的抛出错误;
而用户会持续不断的编码,你可以想一下,输入编写一行代码时,可能只有输入了最后一个分号 `;` 后,编译才会通过,而这期间语言服务器则会持续工作,进程不会停止,而是收集 [`Diagnostic`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnostic),也就是诊断信息,客户端根据这些信息,在代码编辑器上显示<font color="red">红色</font>或者<font color="orange">橙色</font>的波浪线。
<!-- TODO: 补一张编译器的错误和编辑器的波浪线图片 -->

通常来说,编译器能顺利编译通过的工程,语言服务器也不应有诊断的出现。反过来,代码编辑器中没有波浪线,理论上编译也是能过的。
碰到代码编辑器中标红,但能编译通过的情况倒是还好,如果代码编辑器没问题,但编译时报错,那就痛苦了。这种一般都是由于编译器和语言服务器没有统一编译标准导致的,例如两种的编译配置不同,同样一段代码,语言服务器认为是 warning,但编译器认为是 error,甚至是两者的语言版本不同(比如 python2 和 python3)。
另外,像 VS Code 这样的编辑器都支持插件,可能除了语言官方的语言服务器,还有别的(例如 lint 工具)在同时工作,这也会导致代码编辑器里看到的诊断比编译输出的多。
另外,像 VS Code 这样的编辑器都支持插件,可能除了语言官方的语言服务器,还有别的(例如 lint 工具)在同时工作,这也会导致代码编辑器里看到的诊断比编译输出的多。从这个角度上,编译器和语言服务器的一致性也是工程化的一个重要问题。

总的来说,编译器在构建时一次性执行,要求“严谨”,而语言服务器在用户编码时持续服务,要求“健壮”
总的来说,编译器在构建时一次性执行,要求最终正确性和可执行性,而语言服务器在用户编码时持续服务,要求及时性和友好性。

### 使用同一种语言构建

上一章提到,语言服务器与编译器往往是同一种编程语言构建的程序。现在你应该更理解了,在编译原理前端,两种的逻辑高度相似,往后才开始异化。使用同一种语言,甚至在同一个工程中,能最大的复用代码,减少维护成本。
事实上很多语言的编译器是自带语言服务器的,例如 [Deno](https://docs.deno.com/runtime/reference/lsp_integration/)[TypeScript](https://github.com/microsoft/TypeScript/wiki/Standalone-Server-%28tsserver%29)
上一章提到,语言服务器与编译器往往是同一种编程语言构建的程序。现在你应该更理解了,在编译原理前端,两者的逻辑高度相似,往后才开始异化。使用同一种语言,甚至在同一个工程中,能最大的复用代码,减少维护成本。这也是促成语言服务器架构的原因之一
事实上很多语言或是编译器内置语言服务器,例如 [Deno](https://docs.deno.com/runtime/reference/lsp_integration/)[TypeScript](https://github.com/microsoft/TypeScript/wiki/Standalone-Server-%28tsserver%29) ,或是在内置工具链中就有语言服务器,例如 Go 和 Gopls、Rust 和 RLS

当然,语言服务器的实现也并不仅是官方一种,VS Code 就[受不了](https://code.visualstudio.com/api/language-extensions/language-server-extension-guide#error-tolerant-parser-for-language-server) PHP 解析器不能容错,直接新写了一个语言服务器
<!-- TODO: 补一张受不了的截图 -->

## 语法错误与语义错误

语言服务器的编译和编译器的编译的区别:语言服务器服务语言客户端,语言客户端数据驱动,语言服务器只提供智能编程数据。而编译器则需要输出目标代码。编译前端是相似的,词法分析、语法分析、语义分析,这之后两者做的事情就不一样了。
(因此,实现语言服务器和编译器的往往是同一种编程语言。这也是促成语言服务器架构的原因之一)
刚才提到了语法分析和语义分析,这里从诊断的角度再详细说明下。

例子1. hex颜色的格式, `#` + 多个十六进制数。井号后的数字必须是 0-9 或者 a-f,否则是**语法**错误。而井号后只能有3个(rgb)、4个(rgba)、6个(rrggbb)或者8个(rrggbbaa)数字,否则是**语义**错误。语法上定义了hex颜色字面量的语法,但到语义层面,要根据 runtime 决定这个值是否合法

<!-- TODO: 补 color 的语法错误和语义错误截图 -->

例子2

语法错误的影响更严重,例如少了半个括号,会影响后续也许是整个文件的代码的作用域。相比来说,语义错误更好容错,蝴蝶效应较小。

<!-- TODO: 补后括号没写的语法错误,和 int 赋值给 bool 的语法错误 -->

---
草稿

## Volar、Vue 和 Astro

## 如何调试 ts 语言服务器

- 打开 log
- TODO: 见语雀

## 编译:从文本到结构化数据

- 词法分析、语法分析工具
- 语义分析
- 从语义转为智能编程

## LSP 实践

容错:有语法错误:靠词法工具容错。有语义错误:靠语言服务器自身容错。
- 如何读 LSP 文档
- 具体的智能编程功能的实现和踩坑

0 comments on commit 09c6d37

Please sign in to comment.