aura-basetools 0.0.0-stage → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +26 -2
- package/THIRD-PARTY-NOTICES.md +9 -0
- package/bin/aura-mcp.mjs +424 -0
- package/bin/aura.mjs +396 -0
- package/lib/fs-provider.mjs +355 -0
- package/lib/observation-store.mjs +236 -0
- package/lib/ops.mjs +498 -0
- package/lib/transaction-store.mjs +350 -0
- package/lib/workspace-lock.mjs +130 -0
- package/locator-lab/anchor-core.mjs +586 -0
- package/locator-lab/args.mjs +491 -0
- package/locator-lab/handle-core.mjs +207 -0
- package/locator-lab/locator-api.mjs +921 -0
- package/locator-lab/read-core.mjs +3013 -0
- package/locator-lab/repo-map.mjs +768 -0
- package/locator-lab/struct-core.mjs +3026 -0
- package/locator-lab/write-path.mjs +3446 -0
- package/package.json +50 -4
- package/test/standalone-test.mjs +1575 -0
|
@@ -0,0 +1,3026 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* struct-core —— 零依赖、跨语言的结构化定位器。
|
|
3
|
+
*
|
|
4
|
+
* 设计约束(为什么不是别的东西):
|
|
5
|
+
*
|
|
6
|
+
* 1. **不写"每语言一个解析器"。** 那样每加一门语言就要改一处解析代码,是补丁的温床。
|
|
7
|
+
* 这里把差异全部收敛进配置表:注释语法、字符串语法、符号识别规则。扫描器只有一份。
|
|
8
|
+
*
|
|
9
|
+
* 2. **不假装是完整 AST。** 全量语法分析要引依赖(tree-sitter/typescript),且把通用工具
|
|
10
|
+
* 绑死在特定语言上。这里建的是**词法级结构**:token 流 + 括号配平的作用域树 + 声明符号表。
|
|
11
|
+
* 它回答的是定位问题真正需要的那部分——"这个文件里有哪些命名区域、它们从哪到哪"。
|
|
12
|
+
*
|
|
13
|
+
* 3. **不需要时不算。** 建索引有成本,因此只在文本层够不到的时候才建(见 locate 的调用方)。
|
|
14
|
+
*
|
|
15
|
+
* 对外只给三件事:`indexStructure` 建索引、`resolveTarget` 按符号/结构解析目标、
|
|
16
|
+
* `regionConfidence` 给任意区域打分。三者都是纯函数、无 I/O、无语言专属分支。
|
|
17
|
+
* 唯一的内部状态是 `indexStructure` 手里那层**内容派生的记忆化缓存**(见该函数上方注释):
|
|
18
|
+
* 它不改变任何返回值,只是让同一份内容不再被重复解析。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// ── 通用字符扫描器 ─────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* `/` 出现在这里时,它可能是正则的开头吗?
|
|
25
|
+
*
|
|
26
|
+
* 只看 token 流里**前一个 token**,不回溯文本。规则是"前面不可能结束一个表达式":
|
|
27
|
+
* · 没有前一个 token(文件开头)
|
|
28
|
+
* · 前一个是标点,但**不是** `)` `]` `}` `++` `--`(这些能结束一个表达式,
|
|
29
|
+
* 所以后面跟的 `/` 是除号)
|
|
30
|
+
* · 前一个是关键字,且该关键字后面期待一个表达式(`return /x/` `typeof /x/` `case /x/`)
|
|
31
|
+
* 其余情况(前一个是标识符、数字、字符串、正则、或闭合括号)都判为除号。
|
|
32
|
+
*
|
|
33
|
+
* 这是启发式而非精确规则——`if (x) /re/.test(y)` 这种写法会被判成除法。
|
|
34
|
+
* 代价可接受且方向正确:误判成除法的后果是回到**修复前**的行为(正则被当标点),
|
|
35
|
+
* 而误判成正则的后果是吃掉一小段文本。前者是本文件最贵 bug 的成因,因此判据偏向识别正则。
|
|
36
|
+
*
|
|
37
|
+
* @param tokens - 已产出的 token 流
|
|
38
|
+
* @returns 真表示此处的 `/` 应按正则字面量处理
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* 这个 token 出现在这里时,后面还期待一个表达式吗?
|
|
42
|
+
*
|
|
43
|
+
* 同一个问题有两个消费者:`/` 是不是正则的开头,以及换行处表达式要不要续行。
|
|
44
|
+
* 因此只有一份实现——两份实现必然会在某个空白/标点组合上不一致,而那种不一致极难发现。
|
|
45
|
+
*
|
|
46
|
+
* @param prev - 前一个 token(可能不存在)
|
|
47
|
+
* @returns 真表示"后面还期待表达式"
|
|
48
|
+
*/
|
|
49
|
+
function tokenExpectsExpression(prev) {
|
|
50
|
+
if (prev === undefined) return true
|
|
51
|
+
if (prev.type === 'punct') {
|
|
52
|
+
// `)` `]` `}` `++` `--` 能结束一个表达式,因此后面不期待新表达式
|
|
53
|
+
return ![')', ']', '}', '++', '--'].includes(prev.value)
|
|
54
|
+
}
|
|
55
|
+
if (prev.type === 'ident') {
|
|
56
|
+
// 只有"后面期待表达式"的关键字才算;`x / y` 里 x 是标识符 → 不期待
|
|
57
|
+
return REGEX_PRECEDING_KEYWORDS.has(prev.value)
|
|
58
|
+
}
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function previousTokenAllowsRegex(tokens) {
|
|
63
|
+
return tokenExpectsExpression(tokens[tokens.length - 1])
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 前面出现它们时,`/` 更可能是正则的开头而不是除号。
|
|
68
|
+
* 刻意保持很小:这是**词法**判据,不是关键字表,因此不放进按语言的 `keywords`。
|
|
69
|
+
*/
|
|
70
|
+
const REGEX_PRECEDING_KEYWORDS = new Set([
|
|
71
|
+
'return', 'typeof', 'instanceof', 'in', 'of', 'new', 'delete', 'void',
|
|
72
|
+
'case', 'do', 'else', 'yield', 'await', 'throw',
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 把源码扫成 token 流。
|
|
77
|
+
*
|
|
78
|
+
* 语言差异只体现在 `lang` 的注释/字符串配置上;扫描逻辑本身不含任何语言判断。
|
|
79
|
+
* 关键不变式:**注释与字符串已经吃掉了,因此 symbol 规则永远不会被注释里的词或
|
|
80
|
+
* 字符串里的 "function" 触发**——这是它比纯文本匹配结构上更强的地方。
|
|
81
|
+
*
|
|
82
|
+
* @param {string} src - 源码全文
|
|
83
|
+
* @param {object} lang - 语言配置(见 LANGS)
|
|
84
|
+
* @returns {Array<{type, value, line, start, end}>} token 流;type ∈ ident/number/string/punct
|
|
85
|
+
*/
|
|
86
|
+
function tokenize(src, lang) {
|
|
87
|
+
const tokens = []
|
|
88
|
+
const n = src.length
|
|
89
|
+
let i = 0
|
|
90
|
+
let line = 1
|
|
91
|
+
const { lineComments, blockComments, strings, backtick } = lang
|
|
92
|
+
|
|
93
|
+
const push = (type, start, end) => {
|
|
94
|
+
tokens.push({ type, value: src.slice(start, end), line, start, end })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
while (i < n) {
|
|
98
|
+
const c = src[i]
|
|
99
|
+
if (c === '\n') {
|
|
100
|
+
line += 1
|
|
101
|
+
i += 1
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
if (c === ' ' || c === '\t' || c === '\r') {
|
|
105
|
+
i += 1
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 注释:整体吃掉,且**不产出 token**
|
|
110
|
+
let matchedComment = false
|
|
111
|
+
for (const lc of lineComments) {
|
|
112
|
+
if (!src.startsWith(lc, i)) continue
|
|
113
|
+
/**
|
|
114
|
+
* 行注释的**起始限制**:某些语言里注释符在特定上下文中是合法字符。
|
|
115
|
+
*
|
|
116
|
+
* 具体是 shell:`#` 起注释,但 `$#`(参数个数)与 `${#var}`(长度)里它是操作符的一部分。
|
|
117
|
+
* 没有这道限制,`echo $#` 之后的内容会被当注释整行吃掉——而"整行被吃掉"是**静默**的,
|
|
118
|
+
* 表现为后面的声明凭空消失,极难定位。
|
|
119
|
+
*
|
|
120
|
+
* 这是语言相关的真差异,因此与其他差异一样进配置表,不进扫描器逻辑。
|
|
121
|
+
*/
|
|
122
|
+
const blockers = lang.commentBlockers?.[lc]
|
|
123
|
+
if (blockers !== undefined && i > 0 && blockers.includes(src[i - 1])) continue
|
|
124
|
+
while (i < n && src[i] !== '\n') i += 1
|
|
125
|
+
matchedComment = true
|
|
126
|
+
break
|
|
127
|
+
}
|
|
128
|
+
if (matchedComment) continue
|
|
129
|
+
for (const [open, close] of blockComments) {
|
|
130
|
+
if (src.startsWith(open, i)) {
|
|
131
|
+
i += open.length
|
|
132
|
+
while (i < n && !src.startsWith(close, i)) {
|
|
133
|
+
if (src[i] === '\n') line += 1
|
|
134
|
+
i += 1
|
|
135
|
+
}
|
|
136
|
+
i = Math.min(n, i + close.length)
|
|
137
|
+
matchedComment = true
|
|
138
|
+
break
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (matchedComment) continue
|
|
142
|
+
|
|
143
|
+
// 字符串:整体吃掉,产出一个 string token(不暴露内部标识符)
|
|
144
|
+
let matchedString = false
|
|
145
|
+
for (const [open, close] of strings) {
|
|
146
|
+
if (!src.startsWith(open, i)) continue
|
|
147
|
+
const start = i
|
|
148
|
+
i += open.length
|
|
149
|
+
while (i < n) {
|
|
150
|
+
if (src[i] === '\\') {
|
|
151
|
+
i += 2
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
if (src[i] === '\n') line += 1
|
|
155
|
+
if (src.startsWith(close, i) && i > start + open.length - 1) {
|
|
156
|
+
i += close.length
|
|
157
|
+
break
|
|
158
|
+
}
|
|
159
|
+
i += 1
|
|
160
|
+
}
|
|
161
|
+
push('string', start, Math.min(i, n))
|
|
162
|
+
matchedString = true
|
|
163
|
+
break
|
|
164
|
+
}
|
|
165
|
+
if (matchedString) continue
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 正则字面量:整体吃掉。
|
|
169
|
+
*
|
|
170
|
+
* 判据 `previousTokenAllowsRegex` 只看**前一个 token**——这是唯一不需要回溯猜测的通用规则:
|
|
171
|
+
* 只有前面不可能结束一个表达式时,`/` 才可能是正则的开头。
|
|
172
|
+
* 字符类 `[...]` 内部的 `/` 不是结束符(`/[/]/` 合法),因此要单独跟踪。
|
|
173
|
+
* 转义(`\/` `\[`)必须跳过,否则 `\(` 会留下不配对的 open,
|
|
174
|
+
* 而 `matchBrackets` 一旦失衡,其后的声明跨度全部错乱。
|
|
175
|
+
*/
|
|
176
|
+
if (lang.regexLiterals !== undefined && c === lang.regexLiterals.open && previousTokenAllowsRegex(tokens)) {
|
|
177
|
+
const start = i
|
|
178
|
+
i += 1
|
|
179
|
+
let inClass = false
|
|
180
|
+
while (i < n) {
|
|
181
|
+
const rc = src[i]
|
|
182
|
+
if (rc === '\n') break // 未闭合的正则只可能是一行,别吃掉整个文件
|
|
183
|
+
if (rc === lang.regexLiterals.escapes) {
|
|
184
|
+
i += 2
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
if (rc === '[') inClass = true
|
|
188
|
+
else if (rc === ']') inClass = false
|
|
189
|
+
else if (rc === lang.regexLiterals.close && !inClass) {
|
|
190
|
+
i += 1
|
|
191
|
+
break
|
|
192
|
+
}
|
|
193
|
+
i += 1
|
|
194
|
+
}
|
|
195
|
+
push('regex', start, Math.min(i, n))
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 模板串(JS):整体吃掉,但**内部 ${} 不求值**——宁可漏识别,不可误识别
|
|
200
|
+
if (backtick !== undefined && c === backtick.open) {
|
|
201
|
+
const start = i
|
|
202
|
+
i += 1
|
|
203
|
+
while (i < n) {
|
|
204
|
+
if (src[i] === '\\') {
|
|
205
|
+
i += 2
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
if (src[i] === '\n') line += 1
|
|
209
|
+
if (src[i] === backtick.close) {
|
|
210
|
+
i += 1
|
|
211
|
+
break
|
|
212
|
+
}
|
|
213
|
+
i += 1
|
|
214
|
+
}
|
|
215
|
+
push('string', start, Math.min(i, n))
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 标识符:ASCII 字母下划线 + 非 ASCII(覆盖中文标识符)
|
|
220
|
+
if (/[A-Za-z_$\u0080-\uffff]/.test(c)) {
|
|
221
|
+
const start = i
|
|
222
|
+
while (i < n && /[A-Za-z0-9_$\u0080-\uffff]/.test(src[i])) i += 1
|
|
223
|
+
/**
|
|
224
|
+
* **连字符可作为标识符的一部分**(PowerShell 是唯一需要它的语言:`Get-Thing` 是**一个**名字)。
|
|
225
|
+
*
|
|
226
|
+
* 难点是同一个 `-` 在别处是减号。判据不新增名单,而是**复用已有的 `multiPunct` 表**:
|
|
227
|
+
* `-` 后面接的两个字符若是已知的多字符操作符(`-eq` `-ne` `-gt` …),它就是操作符;
|
|
228
|
+
* 否则它是名字的一部分。
|
|
229
|
+
* 于是 `Get-Thing` 里 `-Th` 不是操作符 → 名字继续;`$a -eq $b` 里 `-eq` 是操作符 → 停下。
|
|
230
|
+
*
|
|
231
|
+
* 代价(如实记录):`$a-$b` 这类**无空格减法**会被并成一个标识符。
|
|
232
|
+
* 这是可接受的取舍——误并的后果是少一个符号,而漏掉连字符名字会让 PowerShell
|
|
233
|
+
* 的函数**全部**只剩第一段(实测 `Get-Thing` 被切成 `Get`)。
|
|
234
|
+
*/
|
|
235
|
+
if (lang.dashInIdent === true) {
|
|
236
|
+
while (src[i] === '-' && i + 1 < n && /[A-Za-z_\u0080-\uffff]/.test(src[i + 1])) {
|
|
237
|
+
if (lang.multiPunct.includes(src.slice(i, i + 3))) break
|
|
238
|
+
i += 2
|
|
239
|
+
while (i < n && /[A-Za-z0-9_$\u0080-\uffff]/.test(src[i])) i += 1
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
push('ident', start, i)
|
|
243
|
+
continue
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 数字
|
|
247
|
+
if (/[0-9]/.test(c)) {
|
|
248
|
+
const start = i
|
|
249
|
+
while (i < n && /[0-9a-fA-FxX._]/.test(src[i])) i += 1
|
|
250
|
+
push('number', start, i)
|
|
251
|
+
continue
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 标点:**按表里最长的项优先**(T14)。
|
|
255
|
+
//
|
|
256
|
+
// 先前只看 2 字符窗口,于是表里写的 `===` / `!==` / `...` **永远匹配不到**——
|
|
257
|
+
// 它们会被拆成 `==` + `=` 之类。后果是“表里写了、代码不认”的静默失败:
|
|
258
|
+
// 将来任何测试 `===` 的规则都会永不命中,而看不出原因。
|
|
259
|
+
// 现在按长度降序试:先试 3 字符,再 2,最后 1。
|
|
260
|
+
let matched = 0
|
|
261
|
+
for (const width of [3, 2]) {
|
|
262
|
+
if (i + width > n) continue
|
|
263
|
+
if (lang.multiPunct.includes(src.slice(i, i + width))) {
|
|
264
|
+
push('punct', i, i + width)
|
|
265
|
+
i += width
|
|
266
|
+
matched = width
|
|
267
|
+
break
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (matched > 0) continue
|
|
271
|
+
push('punct', i, i + 1)
|
|
272
|
+
i += 1
|
|
273
|
+
}
|
|
274
|
+
return tokens
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ── 语言配置表(差异只在这里)──────────────────────────────────────────────
|
|
278
|
+
//
|
|
279
|
+
// 每门语言三条信息:注释、字符串、符号规则。**扫描器不含任何语言判断。**
|
|
280
|
+
//
|
|
281
|
+
// 规则里的原语:
|
|
282
|
+
// `'word'` 字面量 token
|
|
283
|
+
// `$name` 捕获符号名(决定声明跨度)
|
|
284
|
+
// `$param` 跳过一个括号组(必选)
|
|
285
|
+
// `$param*` 可选:有括号/泛型就跳,没有就跳过这一段(让有参无参共用一条规则)
|
|
286
|
+
// `$mods*` 可选:吃掉任意个 `modifiers` 里的修饰符(让 public/static/final 组合共用一条规则)
|
|
287
|
+
// `guard` `notKeyword` 拒关键字;`memberName` 拒 `foo.bar(...)` 这种成员访问
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* 正则字面量:**第三种词法类别**,先前配置表里没有它的位置。
|
|
291
|
+
*
|
|
292
|
+
* 这是本文件最贵的一个缺口,实测后果(`read-core.mjs` 自己就是最好的标本):
|
|
293
|
+
* 它 10 个顶层 `function` 声明里,**索引只认出 2 个**,其余 8 个凭空消失
|
|
294
|
+
* (`renderChanged` `renderFull` `renderMap` `renderAround` `renderLines`
|
|
295
|
+
* `renderSearch` `windowEnvelope` `fitWithin`),而已认出的 `renderDigest`
|
|
296
|
+
* 跨度是 `69-69`(真实 69-164)。于是 `aura_edit target=<这些名字>` 全部
|
|
297
|
+
* `STRUCT_NOT_FOUND`,而 `map` 把这些函数报成不存在——**本工具连自己的引擎都定位不了。**
|
|
298
|
+
*
|
|
299
|
+
* 机制:`/` 被当成普通标点,于是
|
|
300
|
+
* · 正则里的引号开了一个**字符串** token,把后面的代码整段吃掉
|
|
301
|
+
* (`/['"]/` 之后 `function after(){}` 消失);
|
|
302
|
+
* · 正则里的转义括号(`\(`)留下一个**不配对的 open**,而每一个
|
|
303
|
+
* `declarationSpan` 与 `$param*` 跳过都依赖 `matchBrackets`——它一旦失衡,
|
|
304
|
+
* 其后的声明跨度全部错乱。
|
|
305
|
+
*
|
|
306
|
+
* **只给真正有正则字面量的语言**(目前是 JS/TS),因此它挂在 `C_LIKE` 而不是
|
|
307
|
+
* `C_LIKE_BASE` 上。Go / Rust / Java / C# / Kotlin / Swift / C / Scala / Dart 都没有
|
|
308
|
+
* 正则字面量,`return a / b` 在它们那里是**除法**;若跟着 base 一起拿到这一项,
|
|
309
|
+
* 那个 `/` 会被当成正则开头并吃掉后面的代码——即用一个新缺陷换掉一个旧缺陷。
|
|
310
|
+
* 这条边界是实测出来的,不是推断:`C_LIKE_BASE` 被 10 个语言共用。
|
|
311
|
+
*/
|
|
312
|
+
const JS_REGEX_LITERALS = { open: '/', close: '/', escapes: '\\' }
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* 换行处**能继续声明头部**的关键字。
|
|
316
|
+
*
|
|
317
|
+
* 头部扫描遇到换行时要回答"上一行的声明还有没有下文":`class A extends B` 后面换行接
|
|
318
|
+
* `implements C {`,那个 `implements` 不是新语句;`const ok = a` 后面换行接 `instanceof B`
|
|
319
|
+
* 同理。这类关键字是语言相关的真差异,因此与 `modifiers`、`bodyOpen` 一样进配置表。
|
|
320
|
+
*
|
|
321
|
+
* 漏列一个的后果是跨度偏短,所以 `scanBodyOpen` 对**非 variable 种类**还有一道自检:
|
|
322
|
+
* 换行处停在一个不是"声明起头"的关键字上时判为不确定 → fail-closed(见该函数)。
|
|
323
|
+
*/
|
|
324
|
+
const C_LIKE_HEADER_KEYWORDS = new Set(['extends', 'implements', 'as', 'satisfies', 'instanceof', 'in', 'of'])
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* 配置里没写 `headerKeywords` 时的默认值:没有跨行子句。
|
|
328
|
+
* 这不是"安全默认",是"如实默认":没有列出来的语言本来就不该在换行处续行。
|
|
329
|
+
* 有跨行子句的语言(`extends` / `implements` / `where`…)必须在配置表里列出来。
|
|
330
|
+
*/
|
|
331
|
+
const NO_HEADER_KEYWORDS = new Set([])
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* 块体**终结规则**:`bodyOpen: null` 的语言靠它把跨度补到真实末端。
|
|
335
|
+
*
|
|
336
|
+
* 这是配置表里第三条语言相关的真差异(前两条是注释与字符串语法)。缺了它,跨度就只有签名行,
|
|
337
|
+
* 而后果是**静默**的:`aura_edit target=<函数名>` 只换掉签名、旧函数体原样留下,
|
|
338
|
+
* `python -m py_compile` 仍然返回 0。因此判据在这里,与其它差异并列:
|
|
339
|
+
*
|
|
340
|
+
* { style: 'indent' } 缩进决定块体(Python / Cython)
|
|
341
|
+
* { style: 'closer', closer, openers, ... } 关键字配对(Ruby / Lua)
|
|
342
|
+
*
|
|
343
|
+
* **两者都没有的语言 = 跨度判不了**:符号被标记 `spanComplete: false`,`target=` 一律拒绝
|
|
344
|
+
* (fail-closed),而不是交出一个偏短的跨度让调用方静默改错地方。
|
|
345
|
+
*
|
|
346
|
+
* `headerEnds` 是这四种差异里的第四种:头部**以什么 token 收尾**、块体从哪一行开始。
|
|
347
|
+
* Python/Cython 是 `:`,OCaml/Haskell 的 `let … =` 是 `=`。缺省是 `:`,方向安全:
|
|
348
|
+
* 某门语言的头若以别的 token 收尾,判据返回 -1 ⇒ fail-closed,而不是猜一个块体起点。
|
|
349
|
+
*/
|
|
350
|
+
const INDENT_BLOCK = { style: 'indent', headerEnds: new Set([':']) }
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* 语句**终结符**收尾的块体(SQL 的 `;`、Erlang 的 `.`、Vim 的 `endfunction`)。
|
|
354
|
+
*
|
|
355
|
+
* 为什么不复用 `closer`:那一种是**开块词与收尾词配对计数**(Ruby 的 `if … end`),
|
|
356
|
+
* 而这里的语言压根没有配对的收尾词——SQL 只有语句终结符,Erlang 的函数子句只有一个 `.`。
|
|
357
|
+
* 硬套 `closer` 要给每条规则编一个假开块词,那是在配置里写谎话。
|
|
358
|
+
*
|
|
359
|
+
* `terminators` 只列**本语言真实**的终结符:漏列只会让跨度偏短(安全方向),
|
|
360
|
+
* 多列则会把 `Erlang 的 x.y` 这种成员访问误当语句尾(跨度偏长,危险方向)。
|
|
361
|
+
*/
|
|
362
|
+
const TERMINATOR_BLOCK = (terminators) => ({ style: 'terminator', terminators: new Set(terminators) })
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* 除 `bodyOpen` 之外还算块开符的字符:`[` —— `const X = [ … ]` 这种字面量体。
|
|
366
|
+
*
|
|
367
|
+
* 刻意**不含 `(`**:`(` 是参数表/调用实参的括号,把它算成块开符会让
|
|
368
|
+
* 每一个“参数表之后没有块括号”的声明塌成一行(Kotlin 的 `fun f(a: Int): Int {`)。
|
|
369
|
+
* 需要用 `(` 当块体的语言(Lisp 家族)在配置里显式写 `bodyOpens: ['(']`。
|
|
370
|
+
*/
|
|
371
|
+
const DEFAULT_BODY_OPENS = new Set(['['])
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* `end` 收尾的块体。字段全是数据,判据在 `closerBlockEnd` 里只有一份:
|
|
375
|
+
* · `openers` 开块词
|
|
376
|
+
* · `closer` 收尾词
|
|
377
|
+
* · `altCloser` 第二种收尾词(Lua 的 `repeat … until`)
|
|
378
|
+
* · `modifiers` 这些词**只有落在子句起始位置**才开块——Ruby 的 `puts x if y` 是后置修饰符
|
|
379
|
+
* · `markers` 跟在 `loopOpeners` 之后时是循环体的引导词,不额外计数(`for i=1,10 do`)
|
|
380
|
+
*/
|
|
381
|
+
const RUBY_BLOCK = {
|
|
382
|
+
style: 'closer',
|
|
383
|
+
closer: 'end',
|
|
384
|
+
openers: new Set(['def', 'class', 'module', 'if', 'unless', 'while', 'until', 'for', 'case', 'begin', 'do']),
|
|
385
|
+
modifiers: new Set(['if', 'unless', 'while', 'until']),
|
|
386
|
+
markers: new Set(['do']),
|
|
387
|
+
loopOpeners: new Set(['for', 'while', 'until']),
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const LUA_BLOCK = {
|
|
391
|
+
style: 'closer',
|
|
392
|
+
closer: 'end',
|
|
393
|
+
altCloser: 'until',
|
|
394
|
+
openers: new Set(['function', 'if', 'for', 'while', 'do', 'repeat']),
|
|
395
|
+
modifiers: new Set([]),
|
|
396
|
+
markers: new Set(['do']),
|
|
397
|
+
loopOpeners: new Set(['for', 'while']),
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** 行注释只有 `//`、字符串只有引号、块体是 `{}` 的语言共用的骨架。 */
|
|
401
|
+
const C_LIKE_BASE = {
|
|
402
|
+
lineComments: ['//'],
|
|
403
|
+
blockComments: [['/*', '*/']],
|
|
404
|
+
strings: [
|
|
405
|
+
['"', '"'],
|
|
406
|
+
["'", "'"],
|
|
407
|
+
],
|
|
408
|
+
multiPunct: [
|
|
409
|
+
'=>', '->', '::', '==', '!=', '<=', '>=', '&&', '||', '?.', '??', '**', '++', '--',
|
|
410
|
+
// 去重:`?.` 此前写了两次。表里的重复项不会报错、只会让人以为改过一处就生效了。
|
|
411
|
+
'+=', '-=', '*=', '/=', '<<', '>>', '...', '===', '!==', '|>',
|
|
412
|
+
],
|
|
413
|
+
brackets: { '(': ')', '[': ']', '{': '}' },
|
|
414
|
+
bodyOpen: '{',
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const C_LIKE = {
|
|
418
|
+
...C_LIKE_BASE,
|
|
419
|
+
regexLiterals: JS_REGEX_LITERALS,
|
|
420
|
+
backtick: { open: '`', close: '`' },
|
|
421
|
+
headerKeywords: C_LIKE_HEADER_KEYWORDS,
|
|
422
|
+
modifiers: new Set(['async', 'static', 'public', 'private', 'protected', 'readonly', 'abstract', 'declare', 'export', 'default']),
|
|
423
|
+
/**
|
|
424
|
+
* 只保留**有明确声明关键字**的形态。刻意不写通用的 `$name ( $param ) {` 作为方法规则——
|
|
425
|
+
* 试过,它把函数参数和对象字面量属性认成方法声明(197 行文件里 29 个假 method)。
|
|
426
|
+
* 代价是对象字面量里的方法无法按名字定位,这是如实的能力边界。
|
|
427
|
+
*/
|
|
428
|
+
rules: [
|
|
429
|
+
{ kind: 'function', pattern: ['$mods*', 'function', '$name', '$param*'] },
|
|
430
|
+
{ kind: 'class', pattern: ['$mods*', 'class', '$name', '$param*'] },
|
|
431
|
+
{ kind: 'variable', pattern: ['$mods*', 'const', '$name'], guard: 'notKeyword' },
|
|
432
|
+
{ kind: 'variable', pattern: ['$mods*', 'let', '$name'], guard: 'notKeyword' },
|
|
433
|
+
{ kind: 'variable', pattern: ['$mods*', 'var', '$name'], guard: 'notKeyword' },
|
|
434
|
+
],
|
|
435
|
+
keywords: new Set([
|
|
436
|
+
'if', 'for', 'while', 'switch', 'catch', 'return', 'function', 'class', 'const', 'let', 'var',
|
|
437
|
+
'new', 'typeof', 'await', 'else', 'do', 'try', 'finally', 'of', 'in', 'async', 'yield',
|
|
438
|
+
'import', 'export', 'from', 'extends', 'super', 'this', 'case', 'break', 'continue', 'delete', 'void',
|
|
439
|
+
]),
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const TYPESCRIPT = {
|
|
443
|
+
...C_LIKE,
|
|
444
|
+
rules: [
|
|
445
|
+
{ kind: 'interface', pattern: ['$mods*', 'interface', '$name', '$param*'] },
|
|
446
|
+
{ kind: 'type', pattern: ['$mods*', 'type', '$name'] },
|
|
447
|
+
{ kind: 'enum', pattern: ['$mods*', 'enum', '$name'] },
|
|
448
|
+
...C_LIKE.rules,
|
|
449
|
+
],
|
|
450
|
+
keywords: new Set([...C_LIKE.keywords, 'interface', 'type', 'enum', 'namespace', 'declare', 'implements', 'readonly', 'abstract', 'keyof', 'infer', 'satisfies']),
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const PYTHON = {
|
|
454
|
+
lineComments: ['#'],
|
|
455
|
+
blockComments: [],
|
|
456
|
+
strings: [
|
|
457
|
+
['"""', '"""'],
|
|
458
|
+
["'''", "'''"],
|
|
459
|
+
['"', '"'],
|
|
460
|
+
["'", "'"],
|
|
461
|
+
],
|
|
462
|
+
multiPunct: ['->', '==', '!=', '<=', '>=', '**', '//', '+=', '-=', '*=', '/=', ':=', '...'],
|
|
463
|
+
brackets: { '(': ')', '[': ']', '{': '}' },
|
|
464
|
+
/** Python 没有块括号:块体由**缩进**给出,因此终结规则进 `blockEnd`,扫描器里不写语言分支。 */
|
|
465
|
+
bodyOpen: null,
|
|
466
|
+
blockEnd: INDENT_BLOCK,
|
|
467
|
+
rules: [
|
|
468
|
+
{ kind: 'class', pattern: ['class', '$name', '$param*'] },
|
|
469
|
+
{ kind: 'function', pattern: ['$mods*', 'def', '$name', '$param*'] },
|
|
470
|
+
],
|
|
471
|
+
modifiers: new Set(['async']),
|
|
472
|
+
keywords: new Set([
|
|
473
|
+
'if', 'elif', 'else', 'for', 'while', 'return', 'def', 'class', 'import', 'from', 'as', 'with',
|
|
474
|
+
'try', 'except', 'finally', 'lambda', 'yield', 'pass', 'raise', 'in', 'not', 'and', 'or', 'is',
|
|
475
|
+
'None', 'True', 'False', 'global', 'nonlocal', 'assert', 'del', 'async', 'await', 'match', 'case',
|
|
476
|
+
]),
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const GO = {
|
|
480
|
+
...C_LIKE_BASE,
|
|
481
|
+
strings: [
|
|
482
|
+
['"', '"'],
|
|
483
|
+
['`', '`'],
|
|
484
|
+
["'", "'"],
|
|
485
|
+
],
|
|
486
|
+
/** Go 的泛型是 `[T any]`,不是 `<>`,所以额外处理方括号。 */
|
|
487
|
+
rules: [
|
|
488
|
+
{ kind: 'function', pattern: ['func', '$name', '$param*'] },
|
|
489
|
+
{ kind: 'method', pattern: ['func', '$param', '$name', '$param*'] },
|
|
490
|
+
{ kind: 'type', pattern: ['type', '$name'] },
|
|
491
|
+
{ kind: 'variable', pattern: ['$mods*', 'var', '$name'], guard: 'notKeyword' },
|
|
492
|
+
{ kind: 'variable', pattern: ['$mods*', 'const', '$name'], guard: 'notKeyword' },
|
|
493
|
+
],
|
|
494
|
+
modifiers: new Set([]),
|
|
495
|
+
keywords: new Set([
|
|
496
|
+
'if', 'for', 'return', 'func', 'type', 'struct', 'interface', 'range', 'go', 'defer', 'chan',
|
|
497
|
+
'select', 'case', 'switch', 'package', 'import', 'var', 'const', 'map', 'default', 'fallthrough', 'goto',
|
|
498
|
+
]),
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const RUST = {
|
|
502
|
+
...C_LIKE_BASE,
|
|
503
|
+
headerKeywords: new Set(['where', 'as', 'for']),
|
|
504
|
+
rules: [
|
|
505
|
+
{ kind: 'function', pattern: ['$mods*', 'fn', '$name', '$param*'] },
|
|
506
|
+
{ kind: 'struct', pattern: ['$mods*', 'struct', '$name', '$param*'] },
|
|
507
|
+
{ kind: 'enum', pattern: ['$mods*', 'enum', '$name', '$param*'] },
|
|
508
|
+
{ kind: 'trait', pattern: ['$mods*', 'trait', '$name', '$param*'] },
|
|
509
|
+
{ kind: 'impl', pattern: ['impl', '$param*', '$name'] },
|
|
510
|
+
{ kind: 'mod', pattern: ['$mods*', 'mod', '$name'] },
|
|
511
|
+
// Rust 的常量/静态绑定。`const`/`static` 已在 modifiers 里,但**规则不能带 `$mods*`**:
|
|
512
|
+
// 那正是 `pub const R: i32 = 1` 之前一个符号都提不出来的原因——
|
|
513
|
+
// 同 `export const` 那个洞(变量规则缺 `$mods*`)同源。
|
|
514
|
+
{ kind: 'variable', pattern: ['$mods*', 'const', '$name'], guard: 'notKeyword' },
|
|
515
|
+
{ kind: 'variable', pattern: ['$mods*', 'static', '$name'], guard: 'notKeyword' },
|
|
516
|
+
],
|
|
517
|
+
modifiers: new Set(['pub', 'unsafe', 'async', 'const', 'extern', 'default']),
|
|
518
|
+
keywords: new Set([
|
|
519
|
+
'fn', 'let', 'mut', 'struct', 'enum', 'trait', 'impl', 'match', 'if', 'else', 'for', 'while',
|
|
520
|
+
'loop', 'return', 'pub', 'use', 'mod', 'where', 'self', 'Self', 'crate', 'super', 'dyn', 'ref',
|
|
521
|
+
'as', 'in', 'move', 'static', 'const', 'type', 'unsafe', 'async', 'await', 'box',
|
|
522
|
+
]),
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const JAVA = {
|
|
526
|
+
...C_LIKE_BASE,
|
|
527
|
+
headerKeywords: new Set(['extends', 'implements', 'throws', 'permits', 'instanceof']),
|
|
528
|
+
/** `@Override` 之类的注解:让扫描器把它们当噪声吃掉,不要卡住声明匹配。 */
|
|
529
|
+
rules: [
|
|
530
|
+
{ kind: 'class', pattern: ['$mods*', 'class', '$name', '$param*'] },
|
|
531
|
+
{ kind: 'interface', pattern: ['$mods*', 'interface', '$name', '$param*'] },
|
|
532
|
+
{ kind: 'enum', pattern: ['$mods*', 'enum', '$name'] },
|
|
533
|
+
{ kind: 'record', pattern: ['$mods*', 'record', '$name', '$param*'] },
|
|
534
|
+
],
|
|
535
|
+
modifiers: new Set([
|
|
536
|
+
'public', 'private', 'protected', 'static', 'final', 'abstract', 'synchronized', 'native',
|
|
537
|
+
'transient', 'volatile', 'strictfp', 'sealed', 'non-sealed', 'default',
|
|
538
|
+
]),
|
|
539
|
+
keywords: new Set([
|
|
540
|
+
'if', 'for', 'while', 'switch', 'case', 'return', 'class', 'interface', 'enum', 'record',
|
|
541
|
+
'new', 'extends', 'implements', 'throws', 'throw', 'try', 'catch', 'finally', 'package',
|
|
542
|
+
'import', 'void', 'int', 'long', 'double', 'float', 'boolean', 'char', 'byte', 'short',
|
|
543
|
+
'public', 'private', 'protected', 'static', 'final', 'abstract', 'synchronized', 'instanceof', 'this', 'super',
|
|
544
|
+
]),
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const CSHARP = {
|
|
548
|
+
...C_LIKE_BASE,
|
|
549
|
+
headerKeywords: new Set(['where', 'is', 'as', 'in']),
|
|
550
|
+
rules: [
|
|
551
|
+
{ kind: 'class', pattern: ['$mods*', 'class', '$name', '$param*'] },
|
|
552
|
+
{ kind: 'interface', pattern: ['$mods*', 'interface', '$name', '$param*'] },
|
|
553
|
+
{ kind: 'enum', pattern: ['$mods*', 'enum', '$name'] },
|
|
554
|
+
{ kind: 'struct', pattern: ['$mods*', 'struct', '$name', '$param*'] },
|
|
555
|
+
{ kind: 'record', pattern: ['$mods*', 'record', '$name', '$param*'] },
|
|
556
|
+
{ kind: 'namespace', pattern: ['namespace', '$name'] },
|
|
557
|
+
],
|
|
558
|
+
modifiers: new Set([
|
|
559
|
+
'public', 'private', 'protected', 'internal', 'static', 'readonly', 'sealed', 'abstract',
|
|
560
|
+
'partial', 'virtual', 'override', 'async', 'extern', 'unsafe', 'new', 'const',
|
|
561
|
+
]),
|
|
562
|
+
keywords: new Set([
|
|
563
|
+
'if', 'for', 'foreach', 'while', 'switch', 'case', 'return', 'class', 'interface', 'enum',
|
|
564
|
+
'struct', 'record', 'namespace', 'new', 'using', 'public', 'private', 'protected', 'internal',
|
|
565
|
+
'static', 'readonly', 'sealed', 'abstract', 'partial', 'virtual', 'override', 'async', 'await',
|
|
566
|
+
'var', 'void', 'int', 'string', 'bool', 'double', 'float', 'decimal', 'in', 'out', 'ref', 'where',
|
|
567
|
+
]),
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const KOTLIN = {
|
|
571
|
+
...C_LIKE_BASE,
|
|
572
|
+
headerKeywords: new Set(['where', 'by', 'as', 'is']),
|
|
573
|
+
rules: [
|
|
574
|
+
{ kind: 'class', pattern: ['$mods*', 'class', '$name', '$param*'] },
|
|
575
|
+
{ kind: 'interface', pattern: ['$mods*', 'interface', '$name', '$param*'] },
|
|
576
|
+
{ kind: 'object', pattern: ['$mods*', 'object', '$name'] },
|
|
577
|
+
{ kind: 'function', pattern: ['$mods*', 'fun', '$param*', '$name'] },
|
|
578
|
+
{ kind: 'variable', pattern: ['$mods*', 'val', '$name'], guard: 'notKeyword' },
|
|
579
|
+
{ kind: 'variable', pattern: ['$mods*', 'var', '$name'], guard: 'notKeyword' },
|
|
580
|
+
],
|
|
581
|
+
modifiers: new Set(['public', 'private', 'protected', 'internal', 'open', 'final', 'abstract', 'sealed', 'data', 'inline', 'suspend', 'operator', 'override', 'lateinit', 'const', 'tailrec', 'external', 'annotation']),
|
|
582
|
+
keywords: new Set([
|
|
583
|
+
'fun', 'class', 'interface', 'object', 'val', 'var', 'if', 'else', 'when', 'for', 'while', 'return',
|
|
584
|
+
'package', 'import', 'is', 'as', 'in', 'out', 'by', 'where', 'init', 'constructor', 'companion',
|
|
585
|
+
'public', 'private', 'protected', 'internal', 'open', 'override', 'data', 'sealed', 'suspend',
|
|
586
|
+
]),
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const SWIFT = {
|
|
590
|
+
...C_LIKE_BASE,
|
|
591
|
+
headerKeywords: new Set(['where', 'throws', 'rethrows', 'async', 'as', 'is']),
|
|
592
|
+
rules: [
|
|
593
|
+
{ kind: 'function', pattern: ['$mods*', 'func', '$name', '$param*'] },
|
|
594
|
+
{ kind: 'class', pattern: ['$mods*', 'class', '$name', '$param*'] },
|
|
595
|
+
{ kind: 'struct', pattern: ['$mods*', 'struct', '$name', '$param*'] },
|
|
596
|
+
{ kind: 'enum', pattern: ['$mods*', 'enum', '$name', '$param*'] },
|
|
597
|
+
{ kind: 'protocol', pattern: ['$mods*', 'protocol', '$name', '$param*'] },
|
|
598
|
+
{ kind: 'extension', pattern: ['$mods*', 'extension', '$name'] },
|
|
599
|
+
{ kind: 'variable', pattern: ['$mods*', 'let', '$name'], guard: 'notKeyword' },
|
|
600
|
+
{ kind: 'variable', pattern: ['$mods*', 'var', '$name'], guard: 'notKeyword' },
|
|
601
|
+
],
|
|
602
|
+
modifiers: new Set(['public', 'private', 'internal', 'fileprivate', 'open', 'final', 'static', 'class', 'mutating', 'nonmutating', 'override', 'required', 'convenience', 'lazy', 'weak', 'unowned', 'indirect', 'prefix', 'postfix']),
|
|
603
|
+
keywords: new Set([
|
|
604
|
+
'func', 'class', 'struct', 'enum', 'protocol', 'extension', 'let', 'var', 'if', 'else', 'guard',
|
|
605
|
+
'switch', 'case', 'for', 'while', 'repeat', 'return', 'import', 'init', 'deinit', 'subscript',
|
|
606
|
+
'public', 'private', 'internal', 'fileprivate', 'open', 'final', 'static', 'override', 'mutating',
|
|
607
|
+
'in', 'where', 'as', 'is', 'try', 'throws', 'async', 'await', 'some', 'any',
|
|
608
|
+
]),
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const C_FAMILY = {
|
|
612
|
+
...C_LIKE_BASE,
|
|
613
|
+
// C++ 的基类表可以写成 `class A :\n public B {`(`:` 是标点,自动续行;`public` 要在这里列)
|
|
614
|
+
headerKeywords: new Set(['public', 'private', 'protected', 'virtual']),
|
|
615
|
+
/** C 预处理指令:整行吃掉,否则 `#define MAX(a,b)` 会被当成函数声明。 */
|
|
616
|
+
lineComments: ['//'],
|
|
617
|
+
strings: [
|
|
618
|
+
['"', '"'],
|
|
619
|
+
["'", "'"],
|
|
620
|
+
],
|
|
621
|
+
rules: [
|
|
622
|
+
{ kind: 'struct', pattern: ['$mods*', 'struct', '$name', '$param*'] },
|
|
623
|
+
{ kind: 'union', pattern: ['$mods*', 'union', '$name'] },
|
|
624
|
+
{ kind: 'enum', pattern: ['$mods*', 'enum', '$name'] },
|
|
625
|
+
{ kind: 'class', pattern: ['$mods*', 'class', '$name', '$param*'] },
|
|
626
|
+
{ kind: 'namespace', pattern: ['namespace', '$name'] },
|
|
627
|
+
/**
|
|
628
|
+
* C/C++ 的函数定义:`ret name(params) {`。
|
|
629
|
+
*
|
|
630
|
+
* 这是"通用形态"唯一一处**配得上**用的场合,因为 C 没有声明关键字,没有别的办法。
|
|
631
|
+
* 与 JS 里被我删掉的那条通用方法规则的区别在于**有硬判别器**:
|
|
632
|
+
* - `$ident*` 吃掉返回类型(`static inline unsigned long *`);它必须是非贪婪的,
|
|
633
|
+
* 否则会把函数名一起吃掉(匹配器为此带回溯,见 {@link matchPattern})
|
|
634
|
+
* - `guard: 'body'` 要求参数表之后必须是 `{`
|
|
635
|
+
* 调用 `foo(x);` 与声明 `int foo(int);` 都以 `;` 结尾,因此被这两道挡在外面。
|
|
636
|
+
*
|
|
637
|
+
* 模式里**不能**写末尾的 `'{'`:守卫要检查的就是那个 token,模式先消费掉它之后
|
|
638
|
+
* 守卫只能看到块体内部(`return`),于是每次匹配都在守卫处失败——这是本规则最隐蔽的坑,
|
|
639
|
+
* 表现为"规则看起来完全正确,却一个函数都索引不到"。
|
|
640
|
+
*/
|
|
641
|
+
{ kind: 'function', pattern: ['$ident*', '$nameNotKeyword', '$param*'], guard: 'body' },
|
|
642
|
+
{ kind: 'typeAlias', pattern: ['typedef', '$ident*', 'struct', '$name'] },
|
|
643
|
+
{ kind: 'typeAlias', pattern: ['typedef', '$ident*', '$name'] },
|
|
644
|
+
],
|
|
645
|
+
/**
|
|
646
|
+
* 只有**修饰符**进这个词表,类型名不进。
|
|
647
|
+
*
|
|
648
|
+
* 曾经把 `int` `char` `unsigned` `long` 也塞进来,结果 `static int bar(int a) {` 里
|
|
649
|
+
* `bar` 的前一个 token 是 `int`,被 `precededByModifier` 闸门误判成"这不是声明起点",
|
|
650
|
+
* 整个函数被丢掉。类型由 `$ident*` 负责,词表只管修饰符——两件事不能混用一个名单。
|
|
651
|
+
*/
|
|
652
|
+
modifiers: new Set(['static', 'inline', 'extern', 'register', 'virtual', 'explicit', 'friend', 'constexpr', 'mutable', 'typename', 'public', 'private', 'protected']),
|
|
653
|
+
keywords: new Set([
|
|
654
|
+
'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'default', 'return', 'struct', 'union',
|
|
655
|
+
'enum', 'class', 'namespace', 'typedef', 'static', 'const', 'extern', 'inline', 'void', 'int',
|
|
656
|
+
'char', 'float', 'double', 'long', 'short', 'unsigned', 'signed', 'sizeof', 'goto', 'break',
|
|
657
|
+
'continue', 'new', 'delete', 'this', 'template', 'try', 'catch', 'throw', 'using', 'operator',
|
|
658
|
+
'public', 'private', 'protected', 'virtual', 'explicit', 'friend', 'constexpr', 'mutable', 'typename', 'auto',
|
|
659
|
+
]),
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const RUBY = {
|
|
663
|
+
lineComments: ['#'],
|
|
664
|
+
blockComments: [['=begin', '=end']],
|
|
665
|
+
strings: [
|
|
666
|
+
['"', '"'],
|
|
667
|
+
["'", "'"],
|
|
668
|
+
],
|
|
669
|
+
multiPunct: ['=>', '==', '!=', '<=', '>=', '<=>', '===', '**', '<<', '>>', '&&', '||', '+=', '-=', '||=', '&&=', '&.'],
|
|
670
|
+
brackets: { '(': ')', '[': ']', '{': '}' },
|
|
671
|
+
/** Ruby 用 `end` 收尾,没有块括号:块体由开块词与 `end` 配对给出。 */
|
|
672
|
+
bodyOpen: null,
|
|
673
|
+
blockEnd: RUBY_BLOCK,
|
|
674
|
+
rules: [
|
|
675
|
+
{ kind: 'class', pattern: ['class', '$name'] },
|
|
676
|
+
{ kind: 'module', pattern: ['module', '$name'] },
|
|
677
|
+
{ kind: 'function', pattern: ['def', '$name', '$param*'] },
|
|
678
|
+
],
|
|
679
|
+
modifiers: new Set([]),
|
|
680
|
+
keywords: new Set([
|
|
681
|
+
'def', 'class', 'module', 'end', 'if', 'elsif', 'else', 'unless', 'while', 'until', 'for', 'do',
|
|
682
|
+
'return', 'yield', 'begin', 'rescue', 'ensure', 'raise', 'require', 'include', 'extend', 'attr_accessor',
|
|
683
|
+
'attr_reader', 'attr_writer', 'self', 'nil', 'true', 'false', 'and', 'or', 'not', 'then', 'case', 'when', 'lambda', 'proc',
|
|
684
|
+
]),
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const PHP = {
|
|
688
|
+
headerKeywords: new Set(['extends', 'implements', 'as', 'instanceof']),
|
|
689
|
+
lineComments: ['//', '#'],
|
|
690
|
+
blockComments: [['/*', '*/']],
|
|
691
|
+
strings: [
|
|
692
|
+
['"', '"'],
|
|
693
|
+
["'", "'"],
|
|
694
|
+
],
|
|
695
|
+
multiPunct: ['=>', '->', '::', '==', '!=', '===', '!==', '<=', '>=', '&&', '||', '??', '**', '+=', '-=', '*=', '/=', '...', '?->'],
|
|
696
|
+
brackets: { '(': ')', '[': ']', '{': '}' },
|
|
697
|
+
bodyOpen: '{',
|
|
698
|
+
rules: [
|
|
699
|
+
{ kind: 'class', pattern: ['$mods*', 'class', '$name', '$param*'] },
|
|
700
|
+
{ kind: 'interface', pattern: ['$mods*', 'interface', '$name', '$param*'] },
|
|
701
|
+
{ kind: 'trait', pattern: ['$mods*', 'trait', '$name'] },
|
|
702
|
+
{ kind: 'enum', pattern: ['$mods*', 'enum', '$name'] },
|
|
703
|
+
{ kind: 'function', pattern: ['$mods*', 'function', '$name', '$param*'] },
|
|
704
|
+
{ kind: 'variable', pattern: ['$mods*', 'const', '$name'], guard: 'notKeyword' },
|
|
705
|
+
],
|
|
706
|
+
modifiers: new Set(['public', 'private', 'protected', 'static', 'final', 'abstract', 'readonly']),
|
|
707
|
+
keywords: new Set([
|
|
708
|
+
'if', 'else', 'elseif', 'for', 'foreach', 'while', 'do', 'switch', 'case', 'return', 'function',
|
|
709
|
+
'class', 'interface', 'trait', 'enum', 'namespace', 'use', 'new', 'public', 'private', 'protected',
|
|
710
|
+
'static', 'final', 'abstract', 'const', 'echo', 'print', 'require', 'include', 'try', 'catch', 'finally',
|
|
711
|
+
'as', 'instanceof', 'clone', 'yield', 'fn', 'match',
|
|
712
|
+
]),
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const SCALA = {
|
|
716
|
+
...C_LIKE_BASE,
|
|
717
|
+
headerKeywords: new Set(['extends', 'with', 'derives', 'as', 'is']),
|
|
718
|
+
rules: [
|
|
719
|
+
{ kind: 'class', pattern: ['$mods*', 'class', '$name', '$param*'] },
|
|
720
|
+
{ kind: 'trait', pattern: ['$mods*', 'trait', '$name', '$param*'] },
|
|
721
|
+
{ kind: 'object', pattern: ['$mods*', 'object', '$name'] },
|
|
722
|
+
{ kind: 'function', pattern: ['$mods*', 'def', '$name', '$param*'] },
|
|
723
|
+
{ kind: 'variable', pattern: ['$mods*', 'val', '$name'], guard: 'notKeyword' },
|
|
724
|
+
{ kind: 'variable', pattern: ['$mods*', 'var', '$name'], guard: 'notKeyword' },
|
|
725
|
+
],
|
|
726
|
+
modifiers: new Set(['private', 'protected', 'override', 'final', 'sealed', 'abstract', 'implicit', 'lazy', 'case']),
|
|
727
|
+
keywords: new Set([
|
|
728
|
+
'class', 'trait', 'object', 'def', 'val', 'var', 'if', 'else', 'match', 'case', 'for', 'while',
|
|
729
|
+
'return', 'package', 'import', 'new', 'extends', 'with', 'yield', 'implicit', 'override', 'sealed', 'abstract',
|
|
730
|
+
]),
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const LUA = {
|
|
734
|
+
lineComments: ['--'],
|
|
735
|
+
blockComments: [['--[[', ']]']],
|
|
736
|
+
strings: [
|
|
737
|
+
['"', '"'],
|
|
738
|
+
["'", "'"],
|
|
739
|
+
['[[', ']]'],
|
|
740
|
+
],
|
|
741
|
+
multiPunct: ['==', '~=', '<=', '>=', '..', '...', '::'],
|
|
742
|
+
brackets: { '(': ')', '[': ']', '{': '}' },
|
|
743
|
+
/** Lua 用 `end`/`until` 收尾:块体由开块词配对给出。 */
|
|
744
|
+
bodyOpen: null,
|
|
745
|
+
blockEnd: LUA_BLOCK,
|
|
746
|
+
rules: [
|
|
747
|
+
{ kind: 'function', pattern: ['function', '$name', '$param*'] },
|
|
748
|
+
{ kind: 'method', pattern: ['function', '$param', '$name', '$param*'] },
|
|
749
|
+
{ kind: 'variable', pattern: ['local', 'function', '$name'] },
|
|
750
|
+
],
|
|
751
|
+
modifiers: new Set(['local']),
|
|
752
|
+
keywords: new Set([
|
|
753
|
+
'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', 'function', 'goto', 'if', 'in',
|
|
754
|
+
'local', 'nil', 'not', 'or', 'repeat', 'return', 'then', 'true', 'until', 'while',
|
|
755
|
+
]),
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const DART = {
|
|
759
|
+
...C_LIKE_BASE,
|
|
760
|
+
headerKeywords: new Set(['extends', 'implements', 'with', 'on', 'async', 'sync', 'as', 'is']),
|
|
761
|
+
rules: [
|
|
762
|
+
{ kind: 'class', pattern: ['$mods*', 'class', '$name', '$param*'] },
|
|
763
|
+
{ kind: 'mixin', pattern: ['$mods*', 'mixin', '$name'] },
|
|
764
|
+
{ kind: 'enum', pattern: ['$mods*', 'enum', '$name'] },
|
|
765
|
+
{ kind: 'extension', pattern: ['$mods*', 'extension', '$name'] },
|
|
766
|
+
{ kind: 'function', pattern: ['$mods*', '$name', '$param', '{'], guard: 'memberName' },
|
|
767
|
+
],
|
|
768
|
+
modifiers: new Set(['abstract', 'static', 'final', 'const', 'external', 'factory', 'late', 'async']),
|
|
769
|
+
keywords: new Set([
|
|
770
|
+
'class', 'mixin', 'enum', 'extension', 'if', 'else', 'for', 'while', 'do', 'switch', 'case',
|
|
771
|
+
'return', 'import', 'library', 'part', 'export', 'new', 'var', 'final', 'const', 'void', 'dynamic',
|
|
772
|
+
'async', 'await', 'yield', 'get', 'set', 'factory', 'abstract', 'static', 'late',
|
|
773
|
+
]),
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const SHELL = {
|
|
777
|
+
lineComments: ['#'],
|
|
778
|
+
blockComments: [],
|
|
779
|
+
strings: [
|
|
780
|
+
['"', '"'],
|
|
781
|
+
["'", "'"],
|
|
782
|
+
],
|
|
783
|
+
/**
|
|
784
|
+
* shell 的 `#` 在这些字符之后**不是**注释:`$#`(参数个数)、`${#var}`(长度)、
|
|
785
|
+
* 以及 `#!`(shebang 由 `#` 起,但那本来就是要吃掉的注释)。
|
|
786
|
+
* 少了这一条,`echo $#` 之后整行被吞——静默丢失,最难查。
|
|
787
|
+
*/
|
|
788
|
+
commentBlockers: { '#': ['$', '{'] },
|
|
789
|
+
/**
|
|
790
|
+
* 刻意**不把 `((` / `))` 当单 token**。
|
|
791
|
+
*
|
|
792
|
+
* 它们是两个独立的开符/闭符:`COMPREPLY=($(compgen … ${cur}))` 里那个 `))` 要关掉两层,
|
|
793
|
+
* 而单 token 的 `))` 一个也关不掉(`closeOf['(']` 是 `)`,不是 `))`),于是栈上留下未配平的 `(`,
|
|
794
|
+
* 函数的 `{` 永远等不到它的 `}`——跨度要么截断(静默)要么判不了(fail-closed)。
|
|
795
|
+
* 去掉这两项后 `((`/`))` 退化成两个 token,配平自然成立(实测:三个 shell completion 脚本)。
|
|
796
|
+
*/
|
|
797
|
+
multiPunct: ['&&', '||', ';;', '>>', '<<', '|&', '&>', '>&', '[[', ']]', '==', '!='],
|
|
798
|
+
/**
|
|
799
|
+
* 括号对。`${var}` 与 `$(cmd)` **不需要**单独列:`$` 先被标识符规则吃掉(`$` 是合法的
|
|
800
|
+
* 标识符起始字符),所以词法器永远不会产出 `${` / `$(` 这样的 token——列了也是死配置。
|
|
801
|
+
* 真正会出现的形状是 `$` + `{` 与 `$` + `(`,它们由普通括号对配平。
|
|
802
|
+
*/
|
|
803
|
+
brackets: { '(': ')', '[': ']', '{': '}' },
|
|
804
|
+
/**
|
|
805
|
+
* shell 的函数体是复合命令 `{ … }`(`fi`/`done` 收的是 `if`/`for`,不是声明)。
|
|
806
|
+
* 因此块括号与其它花括号语言同形;`name() {` 形态把 `{` 吃进了规则模式里,
|
|
807
|
+
* 那一处由 declarationSpan 的“匹配末端就是块开符”处理。
|
|
808
|
+
*/
|
|
809
|
+
bodyOpen: '{',
|
|
810
|
+
rules: [
|
|
811
|
+
// `name() {` 是 POSIX 函数定义;`function name` 是 bash/ksh 风格
|
|
812
|
+
{ kind: 'function', pattern: ['$nameNotKeyword', '$param', '{'], guard: 'memberName' },
|
|
813
|
+
{ kind: 'function', pattern: ['function', '$nameNotKeyword', '$param*'] },
|
|
814
|
+
],
|
|
815
|
+
modifiers: new Set(['function']),
|
|
816
|
+
keywords: new Set([
|
|
817
|
+
'if', 'then', 'else', 'elif', 'fi', 'for', 'while', 'until', 'do', 'done', 'case', 'esac',
|
|
818
|
+
'in', 'function', 'select', 'time', 'coproc', 'return', 'break', 'continue', 'local',
|
|
819
|
+
'export', 'readonly', 'declare', 'typeset', 'unset', 'shift', 'eval', 'exec', 'exit', 'source',
|
|
820
|
+
'set', 'trap', 'wait', 'echo', 'printf', 'read', 'test', 'true', 'false',
|
|
821
|
+
]),
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const POWERSHELL = {
|
|
825
|
+
lineComments: ['#'],
|
|
826
|
+
blockComments: [['<#', '#>']],
|
|
827
|
+
strings: [
|
|
828
|
+
['"', '"'],
|
|
829
|
+
["'", "'"],
|
|
830
|
+
],
|
|
831
|
+
multiPunct: ['||', '&&', '::', '..', '-eq', '-ne', '-gt', '-lt', '-ge', '-le', '-and', '-or', '-not', '-like', '-match', '-replace', '-contains', '-in', '-is', '-as', '+=', '-=', '*=', '/=', '|>', '??'],
|
|
832
|
+
brackets: { '(': ')', '[': ']', '{': '}' },
|
|
833
|
+
bodyOpen: '{',
|
|
834
|
+
/** PowerShell 的函数名可以带连字符:`Get-Thing` 是一个名字,不是 `Get` 减 `Thing`。 */
|
|
835
|
+
dashInIdent: true,
|
|
836
|
+
rules: [
|
|
837
|
+
{ kind: 'class', pattern: ['$mods*', 'class', '$name', '$param*'] },
|
|
838
|
+
{ kind: 'enum', pattern: ['$mods*', 'enum', '$name'] },
|
|
839
|
+
{ kind: 'function', pattern: ['$mods*', 'function', '$name', '$param*'] },
|
|
840
|
+
{ kind: 'function', pattern: ['$mods*', 'filter', '$name', '$param*'] },
|
|
841
|
+
{ kind: 'variable', pattern: ['$mods*', '$name', '='], guard: 'notKeyword' },
|
|
842
|
+
],
|
|
843
|
+
modifiers: new Set(['hidden', 'static', 'global', 'local', 'script', 'private', 'public']),
|
|
844
|
+
keywords: new Set([
|
|
845
|
+
'function', 'filter', 'class', 'enum', 'if', 'else', 'elseif', 'switch', 'foreach', 'for',
|
|
846
|
+
'while', 'do', 'until', 'return', 'break', 'continue', 'try', 'catch', 'finally', 'throw',
|
|
847
|
+
'param', 'process', 'begin', 'end', 'in', 'function', 'using', 'namespace', 'hidden', 'static',
|
|
848
|
+
]),
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const CYTHON = {
|
|
852
|
+
lineComments: ['#'],
|
|
853
|
+
blockComments: [],
|
|
854
|
+
strings: [
|
|
855
|
+
['"""', '"""'],
|
|
856
|
+
["'''", "'''"],
|
|
857
|
+
['"', '"'],
|
|
858
|
+
["'", "'"],
|
|
859
|
+
],
|
|
860
|
+
multiPunct: ['->', '==', '!=', '<=', '>=', '**', '//', '+=', '-=', '*=', '/=', ':=', '||', '|'],
|
|
861
|
+
brackets: { '(': ')', '[': ']', '{': '}' },
|
|
862
|
+
/** 缩进语言:与 Python 同一条规则(Cython 的 `cdef`/`cpdef` 也是缩进块体)。 */
|
|
863
|
+
bodyOpen: null,
|
|
864
|
+
blockEnd: INDENT_BLOCK,
|
|
865
|
+
rules: [
|
|
866
|
+
{ kind: 'class', pattern: ['$mods*', 'class', '$name', '$param*'] },
|
|
867
|
+
{ kind: 'function', pattern: ['def', '$name', '$param*'] },
|
|
868
|
+
{ kind: 'function', pattern: ['cdef', '$name', '$param*'] },
|
|
869
|
+
{ kind: 'function', pattern: ['cpdef', '$name', '$param*'] },
|
|
870
|
+
{ kind: 'type', pattern: ['ctypedef', '$name'] },
|
|
871
|
+
{ kind: 'variable', pattern: ['$mods*', 'cdef', '$name'], guard: 'notKeyword' },
|
|
872
|
+
],
|
|
873
|
+
modifiers: new Set(['async', 'extern', 'public', 'api', 'inline', 'readonly']),
|
|
874
|
+
keywords: new Set([
|
|
875
|
+
'def', 'cdef', 'cpdef', 'ctypedef', 'class', 'if', 'elif', 'else', 'for', 'while', 'return',
|
|
876
|
+
'import', 'from', 'as', 'with', 'try', 'except', 'finally', 'lambda', 'yield', 'pass', 'raise',
|
|
877
|
+
'in', 'not', 'and', 'or', 'is', 'None', 'True', 'False', 'cimport', 'include', 'struct', 'union', 'enum',
|
|
878
|
+
]),
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const PROTO = {
|
|
882
|
+
headerKeywords: new Set(['returns']),
|
|
883
|
+
lineComments: ['//'],
|
|
884
|
+
blockComments: [['/*', '*/']],
|
|
885
|
+
strings: [
|
|
886
|
+
['"', '"'],
|
|
887
|
+
["'", "'"],
|
|
888
|
+
],
|
|
889
|
+
multiPunct: ['==', '!=', '<=', '>=', '||', '&&'],
|
|
890
|
+
brackets: { '(': ')', '[': ']', '{': '}' },
|
|
891
|
+
bodyOpen: '{',
|
|
892
|
+
rules: [
|
|
893
|
+
{ kind: 'message', pattern: ['message', '$name'] },
|
|
894
|
+
{ kind: 'enum', pattern: ['enum', '$name'] },
|
|
895
|
+
{ kind: 'service', pattern: ['service', '$name'] },
|
|
896
|
+
{ kind: 'rpc', pattern: ['rpc', '$name', '$param*'] },
|
|
897
|
+
{ kind: 'oneof', pattern: ['oneof', '$name'] },
|
|
898
|
+
],
|
|
899
|
+
modifiers: new Set(['optional', 'required', 'repeated']),
|
|
900
|
+
keywords: new Set([
|
|
901
|
+
'syntax', 'package', 'import', 'option', 'message', 'enum', 'service', 'rpc', 'returns',
|
|
902
|
+
'oneof', 'map', 'reserved', 'extend', 'extensions', 'stream', 'optional', 'required', 'repeated',
|
|
903
|
+
'public', 'weak', 'true', 'false',
|
|
904
|
+
]),
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const PERL = {
|
|
908
|
+
lineComments: ['#'],
|
|
909
|
+
blockComments: [],
|
|
910
|
+
strings: [
|
|
911
|
+
['"', '"'],
|
|
912
|
+
["'", "'"],
|
|
913
|
+
['`', '`'],
|
|
914
|
+
],
|
|
915
|
+
multiPunct: ['=>', '->', '::', '==', '!=', '<=', '>=', '=~', '!~', '&&', '||', '//', '**', '..', '...', '+=', '-=', '.=', 'x='],
|
|
916
|
+
brackets: { '(': ')', '[': ']', '{': '}' },
|
|
917
|
+
bodyOpen: '{',
|
|
918
|
+
rules: [
|
|
919
|
+
{ kind: 'package', pattern: ['package', '$name'] },
|
|
920
|
+
{ kind: 'function', pattern: ['sub', '$name', '$param*'] },
|
|
921
|
+
{ kind: 'variable', pattern: ['our', '$name'], guard: 'notKeyword' },
|
|
922
|
+
],
|
|
923
|
+
modifiers: new Set(['our', 'my', 'local', 'state', 'sub']),
|
|
924
|
+
keywords: new Set([
|
|
925
|
+
'sub', 'package', 'use', 'require', 'my', 'our', 'local', 'state', 'if', 'elsif', 'else',
|
|
926
|
+
'unless', 'while', 'until', 'for', 'foreach', 'do', 'return', 'last', 'next', 'redo', 'given',
|
|
927
|
+
'when', 'print', 'say', 'printf', 'die', 'warn', 'eval', 'bless', 'ref', 'scalar', 'wantarray',
|
|
928
|
+
'and', 'or', 'not', 'xor', 'eq', 'ne', 'lt', 'gt', 'le', 'ge', 'cmp',
|
|
929
|
+
]),
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const BATCH = {
|
|
933
|
+
lineComments: ['REM ', 'rem ', '::'],
|
|
934
|
+
blockComments: [],
|
|
935
|
+
strings: [
|
|
936
|
+
['"', '"'],
|
|
937
|
+
],
|
|
938
|
+
multiPunct: ['==', '&&', '||', '>>', '2>&1', '>>&1'],
|
|
939
|
+
brackets: { '(': ')', '[': ']' },
|
|
940
|
+
/**
|
|
941
|
+
* batch **刻意不给终结规则**:它的“函数”是约定(`:label` 段 + `goto :eof`),不是语法,
|
|
942
|
+
* 而本配置的 function 规则会把任意裸标识符行都当函数(`set x=1` → 符号 `x`)。
|
|
943
|
+
* 在一个不可信的符号上再套一个猜出来的跨度,只会把“改错地方”从签名行扩大到整段。
|
|
944
|
+
* 因此 .bat 的符号一律 `spanComplete: false` → `target=` 结构化拒绝(fail-closed)。
|
|
945
|
+
*/
|
|
946
|
+
bodyOpen: null,
|
|
947
|
+
rules: [
|
|
948
|
+
{ kind: 'label', pattern: [':', '$name'] },
|
|
949
|
+
{ kind: 'function', pattern: ['$mods*', '$name', '$param*'], guard: 'notKeyword' },
|
|
950
|
+
],
|
|
951
|
+
modifiers: new Set(['call', 'goto']),
|
|
952
|
+
keywords: new Set([
|
|
953
|
+
'if', 'else', 'for', 'in', 'do', 'set', 'setlocal', 'endlocal', 'call', 'goto', 'exit',
|
|
954
|
+
'echo', 'rem', 'shift', 'pushd', 'popd', 'cd', 'copy', 'del', 'move', 'mkdir', 'rmdir',
|
|
955
|
+
'errorlevel', 'exist', 'not', 'defined', 'start', 'type', 'find', 'findstr',
|
|
956
|
+
]),
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* SQL:语句以 `;` 收尾,没有块括号也没有配对收尾词。
|
|
961
|
+
*
|
|
962
|
+
* 规则只收 `CREATE <对象> <名字>`:`table` 在 SQL 里既可以是关键字也可以是表名
|
|
963
|
+
* (`CREATE TABLE table (...)`),而且 `table`/`view` 这几个词本身就是常见列名。
|
|
964
|
+
* 因此它们**不当规则词表**——字面量必须是 `CREATE` 这个不会出现在列名里的词,
|
|
965
|
+
* 否则一个 `CREATE INDEX` 就能在列定义里造出假符号。
|
|
966
|
+
* 代价是 `CREATE PROCEDURE` 这类不认,这是刻意的能力边界。
|
|
967
|
+
*
|
|
968
|
+
* `NULL` 与 `TRUE` 进关键字表:本文件的关键字表就是**模式里的字面量词表**,
|
|
969
|
+
* 它同时承担“这些名字不可能是符号名”的判定(见 `looksLikeBinding`)。
|
|
970
|
+
*/
|
|
971
|
+
const SQL = {
|
|
972
|
+
lineComments: ['--'],
|
|
973
|
+
blockComments: [['/*', '*/']],
|
|
974
|
+
strings: [
|
|
975
|
+
["'", "'"],
|
|
976
|
+
['"', '"'],
|
|
977
|
+
],
|
|
978
|
+
multiPunct: ['||', '::', '>=', '<=', '<>', '!='],
|
|
979
|
+
brackets: { '(': ')', '[': ']', '{': '}' },
|
|
980
|
+
bodyOpen: null,
|
|
981
|
+
blockEnd: TERMINATOR_BLOCK([';']),
|
|
982
|
+
rules: [
|
|
983
|
+
{ kind: 'table', pattern: ['CREATE', 'TABLE', '$mods*', '$name'] },
|
|
984
|
+
{ kind: 'view', pattern: ['CREATE', 'VIEW', '$name'] },
|
|
985
|
+
{ kind: 'index', pattern: ['CREATE', '$mods*', 'INDEX', '$name'] },
|
|
986
|
+
],
|
|
987
|
+
modifiers: new Set(['OR', 'REPLACE', 'TEMP', 'TEMPORARY', 'UNIQUE', 'IF', 'NOT', 'EXISTS']),
|
|
988
|
+
keywords: new Set([
|
|
989
|
+
'select', 'from', 'where', 'group', 'by', 'order', 'having', 'limit', 'offset', 'join', 'inner',
|
|
990
|
+
'left', 'right', 'full', 'outer', 'on', 'as', 'and', 'or', 'not', 'null', 'is', 'in', 'exists',
|
|
991
|
+
'insert', 'into', 'values', 'update', 'set', 'delete', 'create', 'alter', 'drop', 'table', 'view',
|
|
992
|
+
'index', 'primary', 'key', 'foreign', 'references', 'default', 'unique', 'check', 'constraint',
|
|
993
|
+
'case', 'when', 'then', 'else', 'end', 'union', 'all', 'distinct', 'asc', 'desc', 'between', 'like',
|
|
994
|
+
'true', 'false', 'with', 'returning', 'begin', 'commit', 'rollback', 'declare', 'function', 'procedure',
|
|
995
|
+
]),
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* GraphQL SDL:声明是 `type Foo { … }`,块体就是那对花括号。
|
|
1000
|
+
*
|
|
1001
|
+
* `extend` **不进**规则:`extend type Foo @key(...)` 与 `type Foo` 捕获同一个名字,
|
|
1002
|
+
* 两处并存时 `target=Foo` 报歧义。宁可少一个符号,不要一个必然歧义的符号。
|
|
1003
|
+
*/
|
|
1004
|
+
const GRAPHQL = {
|
|
1005
|
+
lineComments: ['#'],
|
|
1006
|
+
blockComments: [],
|
|
1007
|
+
strings: [['"', '"'], ['"""', '"""']],
|
|
1008
|
+
multiPunct: ['...', '&&', '||'],
|
|
1009
|
+
brackets: { '(': ')', '[': ']', '{': '}' },
|
|
1010
|
+
bodyOpen: '{',
|
|
1011
|
+
rules: [
|
|
1012
|
+
{ kind: 'type', pattern: ['type', '$name', '$param*'] },
|
|
1013
|
+
{ kind: 'interface', pattern: ['interface', '$name', '$param*'] },
|
|
1014
|
+
{ kind: 'enum', pattern: ['enum', '$name'] },
|
|
1015
|
+
{ kind: 'schema', pattern: ['schema', '$param*'] },
|
|
1016
|
+
{ kind: 'input', pattern: ['input', '$name'] },
|
|
1017
|
+
{ kind: 'union', pattern: ['union', '$name'] },
|
|
1018
|
+
{ kind: 'directive', pattern: ['directive', '$name', '$param*'] },
|
|
1019
|
+
],
|
|
1020
|
+
modifiers: new Set([
|
|
1021
|
+
'query', 'mutation', 'subscription', 'fragment', 'on', 'implements', 'repeatable',
|
|
1022
|
+
'extend', 'scalar', 'type', 'interface', 'enum', 'union', 'input', 'schema', 'directive',
|
|
1023
|
+
]),
|
|
1024
|
+
keywords: new Set([
|
|
1025
|
+
'query', 'mutation', 'subscription', 'fragment', 'on', 'true', 'false', 'null', 'implements',
|
|
1026
|
+
'type', 'interface', 'enum', 'union', 'input', 'schema', 'directive', 'extend', 'scalar', 'repeatable',
|
|
1027
|
+
]),
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* HCL(Terraform):块名是**带引号的字符串**(`resource "aws_instance" "web"`),
|
|
1032
|
+
* 不是标识符——`$name` 的契约是“只捕标识符”,硬放宽会波及其它 40 余种语言。
|
|
1033
|
+
* 新增的 `$label` 专治这一个形状:它收标识符**或字符串**,并把引号剥掉,
|
|
1034
|
+
* 于是 `.tf` 的块名与其它语言一样是一个可寻址的名字。
|
|
1035
|
+
* 不捕字符串的代价是**一个符号都提不出来**(实测),那比“名字取自哪一个标签”重要得多。
|
|
1036
|
+
*/
|
|
1037
|
+
const TERRAFORM = {
|
|
1038
|
+
lineComments: ['#', '//'],
|
|
1039
|
+
blockComments: [['/*', '*/']],
|
|
1040
|
+
strings: [['"', '"']],
|
|
1041
|
+
multiPunct: ['=>', '==', '!=', '<=', '>=', '&&', '||', '...'],
|
|
1042
|
+
brackets: { '(': ')', '[': ']', '{': '}' },
|
|
1043
|
+
// 块闭符当停止条件:`{ … }` 里换行不是末尾,而 `variable "x" {` 这种只有头部的声明
|
|
1044
|
+
// 也不会卡在 `scanBodyOpen` 的“既不续行也不起声明”分支上(那会让 span 不完整)。
|
|
1045
|
+
bodyOpen: null,
|
|
1046
|
+
blockEnd: TERMINATOR_BLOCK([';']),
|
|
1047
|
+
rules: [
|
|
1048
|
+
// 块名一律用 `$label`:它同时认裸标识符(`variable region`)与带引号的字符串
|
|
1049
|
+
// (`resource "aws_instance" "web"`)。第二种形状里**捕的是第一个标签**,即**类型**
|
|
1050
|
+
// (`aws_instance`)而不是实例名(`web`)。
|
|
1051
|
+
// 这是刻意的取舍:类型名在同一个块里是位置确定的,实例名却在第二个标签上,
|
|
1052
|
+
// 要捕它就得再加一个原语。代价是同类多个块同名(`target=` 报歧义,需行号消歧);
|
|
1053
|
+
// 而“没有名字的符号完全无法寻址”更差。
|
|
1054
|
+
{ kind: 'resource', pattern: ['resource', '$label', '$param*'] },
|
|
1055
|
+
{ kind: 'variable', pattern: ['variable', '$label', '$param*'] },
|
|
1056
|
+
{ kind: 'output', pattern: ['output', '$label', '$param*'] },
|
|
1057
|
+
{ kind: 'module', pattern: ['module', '$label', '$param*'] },
|
|
1058
|
+
// `data` 与 `resource` 同形(`data "aws_ami" "ubuntu" {`),捕第一个标签。
|
|
1059
|
+
{ kind: 'data', pattern: ['data', '$label', '$param*'] },
|
|
1060
|
+
],
|
|
1061
|
+
modifiers: new Set(['resource', 'data', 'variable', 'output', 'module', 'provider', 'locals', 'terraform', 'dynamic', 'for_each']),
|
|
1062
|
+
keywords: new Set([
|
|
1063
|
+
'resource', 'data', 'variable', 'output', 'module', 'provider', 'locals', 'terraform', 'dynamic',
|
|
1064
|
+
'for_each', 'if', 'for', 'in', 'true', 'false', 'null', 'count', 'depends_on', 'lifecycle',
|
|
1065
|
+
]),
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Vue / Svelte 的单文件组件:两者都允许在组件里写真正的 JS。
|
|
1070
|
+
*
|
|
1071
|
+
* 复用 `C_LIKE` 的规则(`const`/`let`/`function`/`class`),换两处:
|
|
1072
|
+
*
|
|
1073
|
+
* 1. **终结规则**换成 `terminator(';')`:这里没有“函数体必须包在花括号里”的保证
|
|
1074
|
+
* (`const title = 'x'` 就是完整声明),而 terminator 自带“行尾就是末尾”的停止条件。
|
|
1075
|
+
* 若改回 `bodyOpen: '{'`,`const title = 'x'` 会停在 `scanBodyOpen` 里一个
|
|
1076
|
+
* 既不续行也不起声明的 token 上,`spanComplete` 变 false,`target=` 被拒。
|
|
1077
|
+
* 2. **关掉正则字面量**。这一条是实测出来的:`<template><div>{{ x }}</div></template>` 里
|
|
1078
|
+
* `</div>` 的前一个 token 是 `>`(能期待表达式),于是 `/div>` 被当成正则吃掉,
|
|
1079
|
+
* 接着 `/template>` 又把**真正的 script 段**一起吞掉——`<script>` 里的 `const`
|
|
1080
|
+
* 凭空消失,一个符号都不剩。宁可把 `/` 当除法:代价只是正则字面量里的引号会被
|
|
1081
|
+
* 误当字符串(影响面极小),而原来那个代价是**整个 script 段不可见**。
|
|
1082
|
+
*/
|
|
1083
|
+
const JS_COMPONENT = {
|
|
1084
|
+
...C_LIKE,
|
|
1085
|
+
regexLiterals: undefined,
|
|
1086
|
+
bodyOpen: null,
|
|
1087
|
+
blockEnd: TERMINATOR_BLOCK([';']),
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Astro:前页(frontmatter)是 JS,正文是 HTML。
|
|
1092
|
+
*
|
|
1093
|
+
* 与 Vue/Svelte 共用规则,但**不能**共用终结规则:`bodyOpen: '{'` 在这里不是取舍而是必要——
|
|
1094
|
+
* `const alpha = () => { return 1 }` 的块体必须由括号配平给出,而 `<div class="x">` 里
|
|
1095
|
+
* 那个 `>` 会被词法器当标点吃掉,不影响配平。
|
|
1096
|
+
*/
|
|
1097
|
+
const ASTRO = {
|
|
1098
|
+
...C_LIKE,
|
|
1099
|
+
bodyOpen: '{',
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/**
|
|
1103
|
+
* R:函数定义就是**赋值**(`alpha <- function(a) { … }`),所以规则用 `$name` + 两个赋值符。
|
|
1104
|
+
* 终结规则复用花括号配平(`bodyOpen: '{'`):R 的函数体一定是 `{ … }`。
|
|
1105
|
+
*/
|
|
1106
|
+
const R = {
|
|
1107
|
+
lineComments: ['#'],
|
|
1108
|
+
blockComments: [],
|
|
1109
|
+
strings: [['"', '"'], ["'", "'"]],
|
|
1110
|
+
multiPunct: ['<-', '->', '<<-', '->>', '==', '!=', '<=', '>=', '&&', '||', ':::', '::', '%>%'],
|
|
1111
|
+
brackets: { '(': ')', '[': ']', '{': '}' },
|
|
1112
|
+
bodyOpen: '{',
|
|
1113
|
+
rules: [
|
|
1114
|
+
{ kind: 'function', pattern: ['$name', '<-', 'function', '$param*'] },
|
|
1115
|
+
{ kind: 'function', pattern: ['$name', '=', 'function', '$param*'] },
|
|
1116
|
+
],
|
|
1117
|
+
modifiers: new Set([]),
|
|
1118
|
+
keywords: new Set([
|
|
1119
|
+
'function', 'if', 'else', 'for', 'while', 'repeat', 'return', 'next', 'break', 'in', 'NULL',
|
|
1120
|
+
'Inf', 'NaN', 'TRUE', 'FALSE', 'library', 'require', 'source',
|
|
1121
|
+
]),
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Elixir:`defmodule Alpha do … end`、`def beta(a) do … end`,与 Ruby 同形的 `do … end` 配对。
|
|
1126
|
+
*
|
|
1127
|
+
* 一行式 `def beta(a), do: a + 1` **不**给跨度:那个 `do:` 不是 `do` token(`do` 与 `:` 分开),
|
|
1128
|
+
* 于是 `closerBlockEnd` 找不到收尾词 ⇒ fail-closed。宁可那一种形态拒绝 `target=`,
|
|
1129
|
+
* 也不交一个猜出来的跨度——它是 Elixir 里最常见的单行写法,所以这条边界必须写进报告。
|
|
1130
|
+
*/
|
|
1131
|
+
/**
|
|
1132
|
+
* Elixir / Ruby 共用 `do … end` 的**形状**,但不共用开块词表。
|
|
1133
|
+
*
|
|
1134
|
+
* Elixir 里 `do` 只是跟在头部后面的**引导词**,块本身由 `defmodule`/`def`/`case` 开。
|
|
1135
|
+
* 把 `do` 也算成开块词会让每个 `def … do` 多计一层,于是模块跨度找不到配平、
|
|
1136
|
+
* `spanComplete` 变 false(实测:`defmodule` 与 `def` 都拒绝 `target=`)。
|
|
1137
|
+
* Ruby 那边**不能**照此改:它的 `do` 出现在 `array.each do` 这类没有别的开块词的写法里,
|
|
1138
|
+
* 少计一层会让那些块提前收尾(`closerBlockEnd` 的缩进校验会把它变成 fail-closed)。
|
|
1139
|
+
*/
|
|
1140
|
+
const ELIXIR_BLOCK = {
|
|
1141
|
+
style: 'closer',
|
|
1142
|
+
closer: 'end',
|
|
1143
|
+
openers: new Set(['def', 'defp', 'defmacro', 'defmodule', 'defprotocol', 'defimpl', 'defstruct', 'defguard', 'if', 'unless', 'case', 'cond', 'with', 'for', 'try', 'receive', 'fn']),
|
|
1144
|
+
modifiers: new Set(['if', 'unless']),
|
|
1145
|
+
markers: new Set(['do']),
|
|
1146
|
+
loopOpeners: new Set(['for', 'case', 'cond', 'with', 'receive']),
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
const ELIXIR = {
|
|
1150
|
+
lineComments: ['#'],
|
|
1151
|
+
blockComments: [],
|
|
1152
|
+
strings: [['"""', '"""'], ['"', '"'], ["'", "'"]],
|
|
1153
|
+
multiPunct: ['->', '<-', '|>', '<>', '==', '!=', '<=', '>=', '&&', '||', '::', '..', '\\'],
|
|
1154
|
+
brackets: { '(': ')', '[': ']', '{': '}' },
|
|
1155
|
+
bodyOpen: null,
|
|
1156
|
+
blockEnd: ELIXIR_BLOCK,
|
|
1157
|
+
rules: [
|
|
1158
|
+
{ kind: 'module', pattern: ['defmodule', '$name', '$param*'] },
|
|
1159
|
+
{ kind: 'function', pattern: ['def', '$nameNotKeyword', '$param*'] },
|
|
1160
|
+
{ kind: 'function', pattern: ['defp', '$nameNotKeyword', '$param*'] },
|
|
1161
|
+
{ kind: 'function', pattern: ['defmacro', '$nameNotKeyword', '$param*'] },
|
|
1162
|
+
],
|
|
1163
|
+
modifiers: new Set(['def', 'defp', 'defmodule', 'defmacro', 'defstruct', 'defprotocol', 'defimpl', 'defguard']),
|
|
1164
|
+
keywords: new Set([
|
|
1165
|
+
'def', 'defp', 'defmodule', 'defmacro', 'defstruct', 'defprotocol', 'defimpl', 'defguard',
|
|
1166
|
+
'do', 'end', 'fn', 'case', 'cond', 'if', 'unless', 'else', 'elsif', 'when', 'with', 'for',
|
|
1167
|
+
'receive', 'try', 'rescue', 'catch', 'after', 'raise', 'import', 'alias', 'use', 'require',
|
|
1168
|
+
'and', 'or', 'not', 'in', 'true', 'false', 'nil',
|
|
1169
|
+
]),
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* Erlang:行首带 `-` 的是**属性**(`-module(alpha).`),不带的是**函数子句**(`beta(A) -> … .`)。
|
|
1174
|
+
*
|
|
1175
|
+
* 终结规则是语句终结符 `.`(不是 `end`):Erlang 的函数子句以 `.` 收尾,属性也是。
|
|
1176
|
+
* `attributeClause` 闸门把“`-` 开头”与“裸名字开头”分开——少了它,`-module(alpha)` 会被
|
|
1177
|
+
* 函数规则命中并报出一个叫 `module` 的假函数。
|
|
1178
|
+
*/
|
|
1179
|
+
const ERLANG = {
|
|
1180
|
+
lineComments: ['%'],
|
|
1181
|
+
blockComments: [],
|
|
1182
|
+
strings: [['"', '"'], ["'", "'"]],
|
|
1183
|
+
multiPunct: ['->', '<-', '==', '/=', '=<', '>=', '++', '--', '::', '||', '|>', '<<', '>>'],
|
|
1184
|
+
brackets: { '(': ')', '[': ']', '{': '}' },
|
|
1185
|
+
bodyOpen: null,
|
|
1186
|
+
blockEnd: TERMINATOR_BLOCK(['.']),
|
|
1187
|
+
rules: [
|
|
1188
|
+
// `$param` 是**跳过**一个括号组、不捕获任何名字,所以属性名必须写 `'(' '$name'`:
|
|
1189
|
+
// 用 `$param` 的结果是 `cap.$name` 为空 ⇒ 符号被 `indexStructure` 静默丢弃(实测 0 个符号)。
|
|
1190
|
+
// 三条属性规则用 `$rest`(主代理为 `.hrl` 加的原语):名字与 `)` 之间的实参可能是
|
|
1191
|
+
// 「逗号 + 字面量」(`-define(ALPHA, 1)`)——`$ident*` 只吃标识符、跳不过去。
|
|
1192
|
+
{ kind: 'module', pattern: ['-', 'module', '(', '$name', '$rest', ')'] },
|
|
1193
|
+
{ kind: 'record', pattern: ['-', 'record', '(', '$name', '$rest', ')'] },
|
|
1194
|
+
{ kind: 'variable', pattern: ['-', 'define', '(', '$name', '$rest', ')'] },
|
|
1195
|
+
{ kind: 'function', pattern: ['$nameNotKeyword', '$param'], guard: 'attributeClause' },
|
|
1196
|
+
],
|
|
1197
|
+
modifiers: new Set([]),
|
|
1198
|
+
keywords: new Set([
|
|
1199
|
+
'module', 'export', 'import', 'record', 'define', 'include', 'behaviour', 'spec', 'type', 'end',
|
|
1200
|
+
'case', 'of', 'if', 'receive', 'after', 'try', 'catch', 'fun', 'when', 'begin', 'andalso', 'orelse',
|
|
1201
|
+
'not', 'div', 'rem', 'band', 'bor', 'bxor', 'bsl', 'bsr',
|
|
1202
|
+
]),
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* Haskell:类型签名(`beta :: Int -> Int`)与函数定义(`beta a = a + 1`)是**两种**声明。
|
|
1207
|
+
*
|
|
1208
|
+
* 缩进决定块体(`blockEnd: INDENT_BLOCK` + `headerEnds: ['=']`):Haskell 的 `=` 就是头部收尾符。
|
|
1209
|
+
* 规则必须**以捕获的名字开头**(没有前导字面量),因为 Haskell 的声明没有关键字可锚定;
|
|
1210
|
+
* 代价是 `a + 1` 这种表达式也满足 `$nameNotKeyword '$param*' '='`——所以 `where`/`let`/`in`
|
|
1211
|
+
* 必须进关键字表,它们是最常见的“裸名字后跟 `=`”的绑定形式。
|
|
1212
|
+
*
|
|
1213
|
+
* **当前状态:未登记**(安全优先,见 LANGS 表里 `hs` 处的注释)。本定义保留作修法依据:
|
|
1214
|
+
* 需要"第一列 layout 守卫"与"签名/实现合并"两件,否则会给假符号或造歧义。
|
|
1215
|
+
*/
|
|
1216
|
+
const HASKELL = {
|
|
1217
|
+
lineComments: ['--'],
|
|
1218
|
+
blockComments: [['{-', '-}']],
|
|
1219
|
+
strings: [['"', '"'], ["'", "'"]],
|
|
1220
|
+
multiPunct: ['->', '<-', '=>', '::', '==', '/=', '<=', '>=', '&&', '||', '..', '>>=', '>>', '.', '$'],
|
|
1221
|
+
brackets: { '(': ')', '[': ']', '{': '}' },
|
|
1222
|
+
bodyOpen: null,
|
|
1223
|
+
blockEnd: { style: 'indent', headerEnds: new Set(['=']) },
|
|
1224
|
+
// 2026-09-22 修(主代理):旧的三条规则会给**假符号**(把函数体内的 `a = a + 1` 当顶层绑定,
|
|
1225
|
+
// 实测报出 `a@4-4`),而假符号比没符号危险。现在两条修法都用上了:
|
|
1226
|
+
// ① `guard: 'topLevel'`(第一列 layout 闸门)区分顶层绑定与函数体内绑定;
|
|
1227
|
+
// ② **去掉 `::` 类型签名规则**:它与实现 `beta a = a + 1` 同名,会造出 `STRUCT_AMBIGUOUS`
|
|
1228
|
+
// (`target=beta` 直接不可用)。代价:只写签名的接口文件不再出符号——如实记录。
|
|
1229
|
+
// `$ident*` 接住裸参数(`beta a = …` 的 `a`),它不是模式的最后一个元素(后面还有 `=`),
|
|
1230
|
+
// 因此不会退化成"跳一个可选括号组"。
|
|
1231
|
+
rules: [
|
|
1232
|
+
{ kind: 'function', pattern: ['$nameNotKeyword', '$ident*', '='], guard: 'topLevel' },
|
|
1233
|
+
],
|
|
1234
|
+
modifiers: new Set([]),
|
|
1235
|
+
keywords: new Set([
|
|
1236
|
+
'module', 'where', 'import', 'qualified', 'as', 'hiding', 'data', 'type', 'newtype', 'class',
|
|
1237
|
+
'instance', 'deriving', 'let', 'in', 'do', 'case', 'of', 'if', 'then', 'else', 'infix', 'infixl',
|
|
1238
|
+
'infixr', 'foreign', 'default', 'True', 'False', 'otherwise',
|
|
1239
|
+
]),
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* OCaml:`let alpha a =`(函数)与 `let x = 1`(绑定)都由 `=` 收尾,块体由缩进给出。
|
|
1244
|
+
*
|
|
1245
|
+
* 两条规则分开写,而不是合成 `$nameNotKeyword '$param*' '='`:后者的 `$param*` 会让
|
|
1246
|
+
* `let alpha a =` 的**参数**先被 `$param*` 吃掉、再由 `$name` 捕到 `a`,于是符号名变成参数名。
|
|
1247
|
+
* 分开写时第一条规则只允许**恰好一个**参数(`$param` 必选),第二条完全没有参数。
|
|
1248
|
+
*/
|
|
1249
|
+
const OCAML = {
|
|
1250
|
+
lineComments: [],
|
|
1251
|
+
blockComments: [['(*', '*)']],
|
|
1252
|
+
strings: [['"', '"']],
|
|
1253
|
+
multiPunct: ['->', '<-', '::', ':=', '==', '!=', '<=', '>=', '&&', '||', '|>', '@@'],
|
|
1254
|
+
brackets: { '(': ')', '[': ']', '{': '}' },
|
|
1255
|
+
bodyOpen: null,
|
|
1256
|
+
blockEnd: { style: 'indent', headerEnds: new Set(['=']) },
|
|
1257
|
+
rules: [
|
|
1258
|
+
{ kind: 'function', pattern: ['let', '$name', '$param', '='] },
|
|
1259
|
+
{ kind: 'variable', pattern: ['let', '$name', '='] },
|
|
1260
|
+
],
|
|
1261
|
+
modifiers: new Set(['rec', 'and', 'mutable', 'private', 'virtual']),
|
|
1262
|
+
keywords: new Set([
|
|
1263
|
+
'let', 'rec', 'and', 'in', 'module', 'open', 'type', 'struct', 'sig', 'end', 'fun', 'function',
|
|
1264
|
+
'match', 'with', 'if', 'then', 'else', 'for', 'while', 'do', 'done', 'begin', 'try', 'exception',
|
|
1265
|
+
'val', 'mutable', 'class', 'object', 'method', 'inherit', 'true', 'false',
|
|
1266
|
+
]),
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Clojure / ClojureScript / Emacs Lisp:声明是**列表**,块体就是那对括号本身。
|
|
1271
|
+
*
|
|
1272
|
+
* `bodyOpens: ['(']` 让 `scanBodyOpen` 把 `(defn beta [a] …)` 的 `(` 当作块开符,
|
|
1273
|
+
* 跨度由配平闭符给出——对 Lisp 来说这是唯一正确的跨度定义(没有别的“体”)。
|
|
1274
|
+
* 规则里刻意**不写 `(`**:字面量写进去会让声明起点落在括号上,跨度反而偏长。
|
|
1275
|
+
*/
|
|
1276
|
+
const LISP = {
|
|
1277
|
+
lineComments: [';'],
|
|
1278
|
+
blockComments: [],
|
|
1279
|
+
strings: [['"', '"']],
|
|
1280
|
+
multiPunct: ['->', '::', '#{', '#(', '~@'],
|
|
1281
|
+
brackets: { '(': ')', '[': ']', '{': '}' },
|
|
1282
|
+
bodyOpen: '(',
|
|
1283
|
+
bodyOpens: new Set(['(']),
|
|
1284
|
+
blockEnd: undefined,
|
|
1285
|
+
rules: [
|
|
1286
|
+
// 末尾**不写** `(`:Clojure 的形参表是 `[a]`(不是 `(a)`),而块体那对括号
|
|
1287
|
+
// 由 `bodyOpen: '('` 给出。写进去会让 `(defn beta [a] …)` 匹配不上(实测 0 个符号)。
|
|
1288
|
+
{ kind: 'namespace', pattern: ['ns', '$nameNotKeyword'] },
|
|
1289
|
+
{ kind: 'function', pattern: ['defn', '$nameNotKeyword'] },
|
|
1290
|
+
{ kind: 'function', pattern: ['defn-', '$nameNotKeyword'] },
|
|
1291
|
+
{ kind: 'macro', pattern: ['defmacro', '$nameNotKeyword'] },
|
|
1292
|
+
{ kind: 'variable', pattern: ['def', '$nameNotKeyword'] },
|
|
1293
|
+
],
|
|
1294
|
+
modifiers: new Set(['defn', 'defn-', 'def', 'defmacro', 'defmulti', 'defmethod', 'defonce', 'ns']),
|
|
1295
|
+
keywords: new Set([
|
|
1296
|
+
'def', 'defn', 'defn-', 'defmacro', 'defmulti', 'defmethod', 'defonce', 'ns', 'let', 'fn', 'if',
|
|
1297
|
+
'do', 'when', 'cond', 'loop', 'recur', 'quote', 'var', 'set!', 'true', 'false', 'nil',
|
|
1298
|
+
]),
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
/**
|
|
1302
|
+
* Vimscript:`function! Alpha(a) … endfunction`,收尾词是关键字而不是 `}` 或 `end`。
|
|
1303
|
+
*
|
|
1304
|
+
* `!` 必须进 `multiPunct` 并在规则里当**字面量**写:`function!` 是一个声明形式,
|
|
1305
|
+
* 而 `!` 默认是单字符标点——写进模式才能同时覆盖 `function` 与 `function!` 两种写法。
|
|
1306
|
+
*/
|
|
1307
|
+
const VIM = {
|
|
1308
|
+
lineComments: ['"'],
|
|
1309
|
+
blockComments: [],
|
|
1310
|
+
strings: [['"', '"'], ["'", "'"]],
|
|
1311
|
+
multiPunct: ['==', '!=', '<=', '>=', '&&', '||', '+=', '-=', '.=', '..', '!~', '=~', '!'],
|
|
1312
|
+
brackets: { '(': ')', '[': ']', '{': '}' },
|
|
1313
|
+
bodyOpen: null,
|
|
1314
|
+
blockEnd: TERMINATOR_BLOCK(['endfunction', 'enddef']),
|
|
1315
|
+
rules: [
|
|
1316
|
+
{ kind: 'function', pattern: ['function', '!', '$nameNotKeyword', '$param*'] },
|
|
1317
|
+
{ kind: 'function', pattern: ['function', '$nameNotKeyword', '$param*'] },
|
|
1318
|
+
],
|
|
1319
|
+
modifiers: new Set(['function', 'function!']),
|
|
1320
|
+
keywords: new Set([
|
|
1321
|
+
'function', 'endfunction', 'def', 'enddef', 'if', 'else', 'elseif', 'endif', 'while', 'endwhile',
|
|
1322
|
+
'for', 'endfor', 'try', 'catch', 'finally', 'endtry', 'return', 'let', 'call', 'set', 'unlet',
|
|
1323
|
+
]),
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
/**
|
|
1327
|
+
* Emacs Lisp:与 Clojure 同形(`(defun alpha (a) …)`),但声明词是 `defun`/`defmacro`/`defvar`。
|
|
1328
|
+
* 注释只有 `;`,字符串只有 `"`——比 Clojure 少一套(没有 `;` 之外的注释起始符)。
|
|
1329
|
+
*/
|
|
1330
|
+
const EMACS_LISP = {
|
|
1331
|
+
...LISP,
|
|
1332
|
+
rules: [
|
|
1333
|
+
// `$param` 紧跟在名字后、且**不是**最后一个元素:这样 `$param*` 才是可选的块开符。
|
|
1334
|
+
// `(defun alpha (a)` 里 `$param*` 先跳过形参表 `(a)`,再吃到函数体的 `(`;
|
|
1335
|
+
// 少了它,跨度只盖住第一行、把第二行的 `)` 留成孤儿(实测 `alpha@1-1`)。
|
|
1336
|
+
{ kind: 'function', pattern: ['defun', '$nameNotKeyword', '$param*', '('] },
|
|
1337
|
+
{ kind: 'macro', pattern: ['defmacro', '$nameNotKeyword', '$param*', '('] },
|
|
1338
|
+
{ kind: 'variable', pattern: ['defvar', '$nameNotKeyword', '$param*'] },
|
|
1339
|
+
{ kind: 'variable', pattern: ['defconst', '$nameNotKeyword', '$param*'] },
|
|
1340
|
+
],
|
|
1341
|
+
modifiers: new Set(['defun', 'defmacro', 'defvar', 'defconst', 'defcustom', 'defsubst']),
|
|
1342
|
+
keywords: new Set([
|
|
1343
|
+
'defun', 'defmacro', 'defvar', 'defconst', 'defcustom', 'defsubst', 'let', 'let*', 'if', 'cond',
|
|
1344
|
+
'when', 'unless', 'while', 'lambda', 'progn', 'setq', 'quote', 't', 'nil',
|
|
1345
|
+
]),
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
1349
|
+
* Nim:`proc alpha(a: int): int =`,块体由**缩进**给出,头部以 `=` 收尾。
|
|
1350
|
+
* 与 OCaml 同样的理由:`$param*` 会把参数吃成名字,所以参数用必选的 `$param`。
|
|
1351
|
+
*/
|
|
1352
|
+
const NIM = {
|
|
1353
|
+
lineComments: ['#'],
|
|
1354
|
+
blockComments: [['#[', ']#']],
|
|
1355
|
+
strings: [['"""', '"""'], ['"', '"'], ["'", "'"]],
|
|
1356
|
+
multiPunct: ['->', '=>', '==', '!=', '<=', '>=', '..', '+=', '-=', '*=', '/=', '&='],
|
|
1357
|
+
brackets: { '(': ')', '[': ']', '{': '}' },
|
|
1358
|
+
bodyOpen: null,
|
|
1359
|
+
blockEnd: { style: 'indent', headerEnds: new Set(['=']) },
|
|
1360
|
+
rules: [
|
|
1361
|
+
// 刻意**不写** `$mods*`:`proc` 同时是模式里的字面量、又在 `modifiers` 词表里,
|
|
1362
|
+
// 于是 `$mods*` 会先把 `proc` 吃掉,后面的字面量 `proc` 再没有 token 可对
|
|
1363
|
+
// (实测:Nim 一个符号都提不出来)。代价是 `export proc` 这类前缀不认,如实记录。
|
|
1364
|
+
// **缩进体语言(`bodyOpen: null`)的写法**:模式里既**不写终结符**也不加 `guard: 'body'`——
|
|
1365
|
+
// 终结符交给 `blockEnd`(Nim 已配 `headerEnds: ['=']`),而 `guard: 'body'` 只适用于大括号体。
|
|
1366
|
+
// 实测:`proc alpha(a: int): int =` → `alpha@1-2`、`target=` 成功;
|
|
1367
|
+
// 之前的 `['proc','$name','$param','=']` 永远匹配不上——参数表与 `=` 之间还夹着**返回类型** `: int`,
|
|
1368
|
+
// 而 `:` 不是标识符、没有原语能跳过它。
|
|
1369
|
+
{ kind: 'function', pattern: ['proc', '$name', '$param'] },
|
|
1370
|
+
{ kind: 'function', pattern: ['proc', '$name', '$param', '='] },
|
|
1371
|
+
{ kind: 'function', pattern: ['func', '$name', '$param', '='] },
|
|
1372
|
+
{ kind: 'function', pattern: ['method', '$name', '$param', '='] },
|
|
1373
|
+
{ kind: 'function', pattern: ['iterator', '$name', '$param', '='] },
|
|
1374
|
+
{ kind: 'type', pattern: ['type', '$name'] },
|
|
1375
|
+
{ kind: 'variable', pattern: ['let', '$name', '='], guard: 'notKeyword' },
|
|
1376
|
+
{ kind: 'variable', pattern: ['var', '$name', '='], guard: 'notKeyword' },
|
|
1377
|
+
],
|
|
1378
|
+
modifiers: new Set([]),
|
|
1379
|
+
keywords: new Set([
|
|
1380
|
+
'proc', 'func', 'method', 'iterator', 'template', 'macro', 'converter', 'type', 'let', 'var',
|
|
1381
|
+
'const', 'if', 'elif', 'else', 'when', 'case', 'of', 'for', 'while', 'return', 'discard', 'import',
|
|
1382
|
+
'from', 'include', 'echo', 'nil', 'true', 'false',
|
|
1383
|
+
]),
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/**
|
|
1387
|
+
* Zig:`fn alpha(a: i32) i32 { … }`——返回类型是**裸标识符**,没有 `:` 也没有 `->`。
|
|
1388
|
+
* 因此函数规则的参数表用 `$param*`(可有可无),后面直接跟块开符 `{`(与 C 家族同形)。
|
|
1389
|
+
* Zig 没有正则字面量,`/` 是除法;注释只有 `//`。
|
|
1390
|
+
*/
|
|
1391
|
+
const ZIG = {
|
|
1392
|
+
lineComments: ['//'],
|
|
1393
|
+
blockComments: [],
|
|
1394
|
+
strings: [['"', '"'], ["'", "'"]],
|
|
1395
|
+
multiPunct: ['=>', '->', '==', '!=', '<=', '>=', '&&', '||', '**', '<<', '>>', '.*', '..', '...', '??', 'orelse', 'catch'],
|
|
1396
|
+
brackets: { '(': ')', '[': ']', '{': '}' },
|
|
1397
|
+
bodyOpen: '{',
|
|
1398
|
+
rules: [
|
|
1399
|
+
// 三条规则覆盖 Zig 的三种函数头:`fn f(a: i32) i32 {`(有参数表 + 返回类型)、
|
|
1400
|
+
// `fn f() void {`(空参数表 + 返回类型)、`fn f() {`(无返回类型)。
|
|
1401
|
+
//
|
|
1402
|
+
// **不能用 `$param*` 把前两种合成一条**:`$param*` 跳完参数组后若下一个 token 仍不是 `{`,
|
|
1403
|
+
// 它会回溯到「不跳」的分支去要求 `{`(那时 token 是 `(`)——整条规则作废,实测 0 个符号。
|
|
1404
|
+
// 返回类型用 `$ident*`(非贪婪吃标识符)接住,`$name` 因此仍捕到函数名而不是类型名。
|
|
1405
|
+
// `$ident*` 后面接 `{` 时,`$ident*` 不能是**最后一个**元素——那会让它退化成
|
|
1406
|
+
// 「匹配一个可选括号组」而不是「吃掉若干标识符」(`$ident*` 与 `$param*` 共用一条
|
|
1407
|
+
// skipOptional 分支,末尾位置的分支语义不同,实测两条规则都匹配不上 ⇒ 0 个符号)。
|
|
1408
|
+
// **块开符不能写进模式**:C 家族就是这么写的(`['$ident*','$nameNotKeyword','$param*']` + `guard: 'body'`),
|
|
1409
|
+
// 块开符交给 `bodyOpen` 判。之前三条都写了 `{`,所以一个都匹配不上。
|
|
1410
|
+
// 实测覆盖三种头:`fn f(a: i32) i32 {`、`fn f() void {`、`fn f() {` → 均得 `名字@1-3` 且 `target=` 成功。
|
|
1411
|
+
{ kind: 'function', pattern: ['fn', '$name', '$param', '$ident*'], guard: 'body' },
|
|
1412
|
+
{ kind: 'function', pattern: ['fn', '$name', '$ident*', '$param', '{'], guard: 'body' },
|
|
1413
|
+
{ kind: 'function', pattern: ['fn', '$name', '$ident*', '{'], guard: 'body' },
|
|
1414
|
+
{ kind: 'function', pattern: ['fn', '$name', '$param', '{'], guard: 'body' },
|
|
1415
|
+
{ kind: 'variable', pattern: ['const', '$nameNotKeyword'] },
|
|
1416
|
+
{ kind: 'variable', pattern: ['var', '$nameNotKeyword'] },
|
|
1417
|
+
],
|
|
1418
|
+
modifiers: new Set(['pub', 'extern', 'export', 'inline', 'noinline', 'comptime']),
|
|
1419
|
+
keywords: new Set([
|
|
1420
|
+
'fn', 'const', 'var', 'pub', 'if', 'else', 'while', 'for', 'switch', 'return', 'break', 'continue',
|
|
1421
|
+
'defer', 'errdefer', 'try', 'catch', 'orelse', 'struct', 'enum', 'union', 'error', 'comptime',
|
|
1422
|
+
'test', 'usingnamespace', 'export', 'extern', 'inline', 'noinline', 'true', 'false', 'null',
|
|
1423
|
+
]),
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
export const LANGS = {
|
|
1427
|
+
js: C_LIKE,
|
|
1428
|
+
mjs: C_LIKE,
|
|
1429
|
+
cjs: C_LIKE,
|
|
1430
|
+
jsx: C_LIKE,
|
|
1431
|
+
ts: TYPESCRIPT,
|
|
1432
|
+
tsx: TYPESCRIPT,
|
|
1433
|
+
mts: TYPESCRIPT,
|
|
1434
|
+
cts: TYPESCRIPT,
|
|
1435
|
+
py: PYTHON,
|
|
1436
|
+
pyi: PYTHON,
|
|
1437
|
+
go: GO,
|
|
1438
|
+
rs: RUST,
|
|
1439
|
+
java: JAVA,
|
|
1440
|
+
cs: CSHARP,
|
|
1441
|
+
kt: KOTLIN,
|
|
1442
|
+
kts: KOTLIN,
|
|
1443
|
+
swift: SWIFT,
|
|
1444
|
+
c: C_FAMILY,
|
|
1445
|
+
h: C_FAMILY,
|
|
1446
|
+
cc: C_FAMILY,
|
|
1447
|
+
cpp: C_FAMILY,
|
|
1448
|
+
cxx: C_FAMILY,
|
|
1449
|
+
hpp: C_FAMILY,
|
|
1450
|
+
hh: C_FAMILY,
|
|
1451
|
+
rb: RUBY,
|
|
1452
|
+
php: PHP,
|
|
1453
|
+
scala: SCALA,
|
|
1454
|
+
lua: LUA,
|
|
1455
|
+
dart: DART,
|
|
1456
|
+
// 按本机真实文件密度补的第二批
|
|
1457
|
+
sh: SHELL,
|
|
1458
|
+
bash: SHELL,
|
|
1459
|
+
zsh: SHELL,
|
|
1460
|
+
ksh: SHELL,
|
|
1461
|
+
ps1: POWERSHELL,
|
|
1462
|
+
psm1: POWERSHELL,
|
|
1463
|
+
psd1: POWERSHELL,
|
|
1464
|
+
pyx: CYTHON,
|
|
1465
|
+
pxd: CYTHON,
|
|
1466
|
+
pxi: CYTHON,
|
|
1467
|
+
proto: PROTO,
|
|
1468
|
+
pl: PERL,
|
|
1469
|
+
pm: PERL,
|
|
1470
|
+
bat: BATCH,
|
|
1471
|
+
cmd: BATCH,
|
|
1472
|
+
// 第三批:验收清单点名、而前两批漏掉的常见格式(它们的缺失代价是 `target=` 直接
|
|
1473
|
+
// LOCATOR_NO_LANGUAGE,连符号表都没有)。`gql` 与 `graphql`、`ex` 与 `exs` 同义。
|
|
1474
|
+
sql: SQL,
|
|
1475
|
+
graphql: GRAPHQL,
|
|
1476
|
+
gql: GRAPHQL,
|
|
1477
|
+
vue: JS_COMPONENT,
|
|
1478
|
+
svelte: JS_COMPONENT,
|
|
1479
|
+
astro: ASTRO,
|
|
1480
|
+
tf: TERRAFORM,
|
|
1481
|
+
r: R,
|
|
1482
|
+
ex: ELIXIR,
|
|
1483
|
+
exs: ELIXIR,
|
|
1484
|
+
clj: LISP,
|
|
1485
|
+
cljs: LISP,
|
|
1486
|
+
el: EMACS_LISP,
|
|
1487
|
+
vim: VIM,
|
|
1488
|
+
cython: CYTHON,
|
|
1489
|
+
// `.erl`/`.hs`/`.ml` 登记但**不出符号**(跨度判据没做对,见交付报告):登记的理由是
|
|
1490
|
+
// "格式认得出来"——不登记的话 `target=` 会退化成 `LOCATOR_NO_LANGUAGE`(那个错误码的含义是
|
|
1491
|
+
// "这个扩展名不认识"),而事实是认识、只是索引不出东西;两种失败必须可区分。
|
|
1492
|
+
// 注(2026-09-22 更新):`nim`/`zig` 已修好并登记(见各自定义里的写法注释),`hrl` 仍未登记
|
|
1493
|
+
// ——它需要引擎原语层的改造(见 map 末尾那条注释),不是配置能补的。
|
|
1494
|
+
erl: ERLANG,
|
|
1495
|
+
// `hs` 加回来了(2026-09-22):两条修法都落地——`guard: 'topLevel'`(第一列 layout 闸门,
|
|
1496
|
+
// 见 matchRule 里的注释)区分顶层绑定与函数体内绑定;并去掉 `::` 签名规则以免与实现同名造歧义。
|
|
1497
|
+
hs: HASKELL,
|
|
1498
|
+
ml: OCAML,
|
|
1499
|
+
// `hs` **刻意不登记**(安全优先):它当前的规则会给出**假符号**——
|
|
1500
|
+
// `['$nameNotKeyword','$param','=']` 匹配不了 `beta a = a + 1`(Haskell 的参数是裸标识符、
|
|
1501
|
+
// 不是括号组),而 `['$nameNotKeyword','=']` 会把函数体里的 `a = a + 1` 当成顶层绑定
|
|
1502
|
+
// (实测符号表里出现 `a@4-4`)。**假符号比没符号危险**:`target=a` 会改到参数身上。
|
|
1503
|
+
// 正确修法(两件,属规则层、不是原语层):
|
|
1504
|
+
// ① 用 **layout(第一列)守卫**区分顶层绑定与函数体内的绑定;
|
|
1505
|
+
// ② 类型签名 `beta :: Int -> Int` 与实现 `beta a = a + 1` 同名 → 两条规则会造出
|
|
1506
|
+
// `STRUCT_AMBIGUOUS`,需要"签名与实现合并成一个符号"(或只取实现那条)。
|
|
1507
|
+
// HASKELL 的定义先留着(带修法注释),修好后再加回这张表。
|
|
1508
|
+
nim: NIM,
|
|
1509
|
+
zig: ZIG,
|
|
1510
|
+
// `hrl` 与 `erl` 同一套规则:`.hrl` 就是 Erlang 头文件。它的常见形态需要 `$rest` 原语
|
|
1511
|
+
// (名字与 `)` 之间夹着「逗号 + 字面量」),该原语已加到匹配器里,见 `skipToGroupClose`。
|
|
1512
|
+
hrl: ERLANG,
|
|
1513
|
+
// `hrl` 仍**不登记**:`.hrl` 的常见形态是 `-define(NAME, 1).` / `-record(name, {...}).`,
|
|
1514
|
+
// 名字与 `)` 之间夹着「逗号 + 字面量」,而现有原语(`$param` 跳括号组、`$ident*` 吃标识符、
|
|
1515
|
+
// `$mods*` 吃词表、`$label` 捕字符串)**都跳不过它**(两种写法都实测过:加 `')'` 匹配失败、
|
|
1516
|
+
// 不加则拿不到符号)。正确修法是给匹配器加一个"跳到当前括号组收尾"的原语(或让 `declarationSpan`
|
|
1517
|
+
// 容忍未闭合的组),属**引擎原语层**改造,另开工作令。
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
/** 只按扩展名选语言;不认识就返回 null,调用方据此放弃结构化路径而不是猜。 */
|
|
1521
|
+
export function langOf(filePath) {
|
|
1522
|
+
const m = /\.([A-Za-z0-9]+)$/.exec(filePath ?? '')
|
|
1523
|
+
if (m === null) return null
|
|
1524
|
+
return LANGS[m[1].toLowerCase()] ?? null
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// ── 结构索引 ───────────────────────────────────────────────────────────────
|
|
1528
|
+
|
|
1529
|
+
/** 从 token 位置反推行号的二分查找表:建一次,查询 O(log n)。 */
|
|
1530
|
+
function lineIndexFor(src) {
|
|
1531
|
+
const starts = [0]
|
|
1532
|
+
for (let i = 0; i < src.length; i += 1) if (src[i] === '\n') starts.push(i + 1)
|
|
1533
|
+
return (offset) => {
|
|
1534
|
+
let lo = 0
|
|
1535
|
+
let hi = starts.length - 1
|
|
1536
|
+
while (lo < hi) {
|
|
1537
|
+
const mid = (lo + hi + 1) >> 1
|
|
1538
|
+
if (starts[mid] <= offset) lo = mid
|
|
1539
|
+
else hi = mid - 1
|
|
1540
|
+
}
|
|
1541
|
+
return lo + 1
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
/**
|
|
1546
|
+
* 括号配平,得到每个开括号 token 的作用域跨度。
|
|
1547
|
+
* 语言无关——`brackets` 表把 `(` `[` `{` 映射到各自的闭括号。
|
|
1548
|
+
* @returns {Map<number, number>} 开 token 下标 -> 闭 token 下标
|
|
1549
|
+
*/
|
|
1550
|
+
function matchBrackets(tokens, lang) {
|
|
1551
|
+
const pairs = new Map()
|
|
1552
|
+
const stack = []
|
|
1553
|
+
const closeOf = lang.brackets
|
|
1554
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
1555
|
+
const t = tokens[i]
|
|
1556
|
+
if (t.type !== 'punct') continue
|
|
1557
|
+
if (closeOf[t.value] !== undefined) stack.push(i)
|
|
1558
|
+
else if (stack.length > 0 && tokens[stack[stack.length - 1]].value !== undefined && closeOf[tokens[stack[stack.length - 1]].value] === t.value) {
|
|
1559
|
+
pairs.set(stack.pop(), i)
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
return pairs
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
/** 跳过一个配平括号组(或单个 token),返回其后的下标。 */
|
|
1566
|
+
function skipGroup(tokens, k, pairs) {
|
|
1567
|
+
const t = tokens[k]
|
|
1568
|
+
if (t !== undefined && t.type === 'punct' && pairs.has(k)) return pairs.get(k) + 1
|
|
1569
|
+
return k + 1
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
/**
|
|
1573
|
+
* 泛型参数表的 `<>` 配平。
|
|
1574
|
+
*
|
|
1575
|
+
* 为什么不能进 `brackets` 表统一处理:`<` 与 `>` 同时是**小于/大于运算符**,
|
|
1576
|
+
* 全局配平会把 `a < b` 到某个 `>` 之间的整段代码当成泛型参数吞掉。
|
|
1577
|
+
* 因此它只在**声明头部**被显式调用(`$param*` 里),并带三道保险:
|
|
1578
|
+
* - 嵌套计数必须回到 0
|
|
1579
|
+
* - 扫过的 token 里不允许出现语句终结符(`;` `{` `}`),出现即判定这不是泛型参数表
|
|
1580
|
+
* - token 数上限 `MAX_ANGLE_SCAN`,挡住"文件里全是 `<` 却始终配不平"的病态扫描
|
|
1581
|
+
*
|
|
1582
|
+
* 上限是**防御性**的,不是语义边界:正常的泛型参数表在几十个 token 内必定闭合。
|
|
1583
|
+
* 把它写成命名常量而不是裸 `64`,是因为一个没有名字、没有理由的数字,
|
|
1584
|
+
* 下一个读代码的人只能猜它是不是可以改。
|
|
1585
|
+
*/
|
|
1586
|
+
const MAX_ANGLE_SCAN = 64
|
|
1587
|
+
|
|
1588
|
+
function matchAngle(tokens, open) {
|
|
1589
|
+
let depth = 0
|
|
1590
|
+
for (let k = open; k < tokens.length && k <= open + MAX_ANGLE_SCAN; k += 1) {
|
|
1591
|
+
const t = tokens[k]
|
|
1592
|
+
if (t.type !== 'punct') continue
|
|
1593
|
+
if (t.value === '<') depth += 1
|
|
1594
|
+
else if (t.value === '>') {
|
|
1595
|
+
depth -= 1
|
|
1596
|
+
if (depth === 0) return k
|
|
1597
|
+
} else if (t.value === ';' || t.value === '{' || t.value === '}') return -1
|
|
1598
|
+
}
|
|
1599
|
+
return -1
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
/**
|
|
1603
|
+
* 按模式匹配一条规则,**带回溯**。
|
|
1604
|
+
*
|
|
1605
|
+
* 为什么必须回溯:`$ident*`(吃掉任意个非关键字标识符)是唯一能表达 C 返回类型的办法,
|
|
1606
|
+
* 而它**必须是非贪婪的**——`int foo(void) {` 里若先把 `int` 和 `foo` 都吃掉,后面的 `$name`
|
|
1607
|
+
* 就没东西可捕。贪婪版本需要"匹配完再退回一步"这种补丁式修补;回溯是同一件事的干净写法。
|
|
1608
|
+
*
|
|
1609
|
+
* 模式原语:
|
|
1610
|
+
* `'word'` 字面量 token,必须相等
|
|
1611
|
+
* `$name` 捕获符号名(必须是标识符;决定声明起点)
|
|
1612
|
+
* `$param` 跳过一个配平括号组(必选)
|
|
1613
|
+
* `$param*` 可选:有 `()` 或 `<>` 就跳,没有就跳过这一段
|
|
1614
|
+
* `$mods*` 可选:吃掉任意个 `modifiers` 词表里的修饰符
|
|
1615
|
+
* `$ident*` 可选(**非贪婪**):吃掉任意个非关键字标识符
|
|
1616
|
+
* `$rest` 跳到**当前所在括号组的收尾 token**(见 `skipToGroupClose`)
|
|
1617
|
+
*
|
|
1618
|
+
* @returns {{cap, end}|null} cap 是捕获到的命名段;end 是规则末端 token 下标
|
|
1619
|
+
*/
|
|
1620
|
+
/**
|
|
1621
|
+
* 跳到**当前所在括号组的收尾 token**(`)` 那一个)。
|
|
1622
|
+
*
|
|
1623
|
+
* 为什么需要这个原语:Erlang 头文件的 `-define(ALPHA, 1).` / `-record(alpha, {beta = 1}).`
|
|
1624
|
+
* 里,名字与 `)` 之间夹着「**逗号 + 字面量**」。现有的四个原语都跳不过它——
|
|
1625
|
+
* `$ident*` 只吃标识符、`$param`/`$param*` 只跳**完整的**括号组、`$mods*` 只吃修饰词表。
|
|
1626
|
+
* 实测两种旧写法都失败:加 `')'` 匹配不上、不加则拿不到符号。
|
|
1627
|
+
*
|
|
1628
|
+
* 做法:配对表 `pairs` 里找**包含当前位置的最内层**组,跳到它的收尾 token,
|
|
1629
|
+
* 让模式里接下来的字面量 `)` 去对上。返回 -1 表示当前不在任何括号组内(规则不匹配)。
|
|
1630
|
+
*/
|
|
1631
|
+
function skipToGroupClose(tokens, start, pairs) {
|
|
1632
|
+
let bestOpen = -1
|
|
1633
|
+
let bestClose = -1
|
|
1634
|
+
for (const [open, close] of pairs) {
|
|
1635
|
+
if (open < start && start <= close && open > bestOpen) {
|
|
1636
|
+
bestOpen = open
|
|
1637
|
+
bestClose = close
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
return bestClose
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
function matchPattern(tokens, start, pattern, pi, cap, lang, pairs) {
|
|
1644
|
+
if (pi === pattern.length) return { cap, end: start }
|
|
1645
|
+
const part = pattern[pi]
|
|
1646
|
+
const t = tokens[start]
|
|
1647
|
+
|
|
1648
|
+
if (part === '$param*' || part === '$mods*' || part === '$ident*') {
|
|
1649
|
+
// 先试"跳"的版本,失败再试"不跳"的版本——这就是回溯
|
|
1650
|
+
const advanced = skipOptional(tokens, start, part, lang, pairs)
|
|
1651
|
+
if (advanced !== start) {
|
|
1652
|
+
const hit = matchPattern(tokens, advanced, pattern, pi + 1, cap, lang, pairs)
|
|
1653
|
+
if (hit !== null) return hit
|
|
1654
|
+
}
|
|
1655
|
+
return matchPattern(tokens, start, pattern, pi + 1, cap, lang, pairs)
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
if (t === undefined) return null
|
|
1659
|
+
if (part.startsWith('$')) {
|
|
1660
|
+
if (part.startsWith('$param')) {
|
|
1661
|
+
return matchPattern(tokens, skipGroup(tokens, start, pairs), pattern, pi + 1, cap, lang, pairs)
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* `$label`:块名可以是一个**带引号的字符串**(HCL 的 `resource "aws_type" "name"`)。
|
|
1665
|
+
* 把它单独作为一种原语,而不是把 `$name` 放宽到任意 token——`$name` 的“只捕标识符”
|
|
1666
|
+
* 是其它 40 余种语言的跨度根基,放宽它等于把所有语言的规则一起改掉。
|
|
1667
|
+
* 剥引号后仍要求像标识符:`""` 与 `"a b"` 不是名字,宁可这条规则不匹配。
|
|
1668
|
+
*/
|
|
1669
|
+
if (part === '$rest') {
|
|
1670
|
+
const close = skipToGroupClose(tokens, start, pairs)
|
|
1671
|
+
if (close === -1) return null
|
|
1672
|
+
return matchPattern(tokens, close, pattern, pi + 1, cap, lang, pairs)
|
|
1673
|
+
}
|
|
1674
|
+
if (part === '$label') {
|
|
1675
|
+
// 捕获值先保持**原样**(含引号):`matchRule` 靠“值相等”反查名字 token 来定声明起点,
|
|
1676
|
+
// 在这里剥引号会让那次反查找不到 token(实测:起点退回模式起点,绑定闸门误杀整条规则)。
|
|
1677
|
+
// 剥引号的动作放在 `indexStructure` 推符号之前,两处各管一件事。
|
|
1678
|
+
if (t.type !== 'ident' && t.type !== 'string') return null
|
|
1679
|
+
return matchPattern(tokens, start + 1, pattern, pi + 1, { ...cap, $name: t.value }, lang, pairs)
|
|
1680
|
+
}
|
|
1681
|
+
if (t.type !== 'ident') return null
|
|
1682
|
+
// 名字的关键字约束必须**在这里**判,不能放到事后的 guard 里。
|
|
1683
|
+
// 事后 guard 只能返回 null,而 null 会让整条规则回溯到"不跳"的分支——它无法让
|
|
1684
|
+
// `$ident*` 换个长度重试。实测后果:`int foo(void) {` 里 `$ident*` 先贪婪吃下
|
|
1685
|
+
// `int foo`,`$name` 捕到 `foo` 是对的,但事后按名字反查找到了第一个叫 `int` 的 token,
|
|
1686
|
+
// 判成关键字,整条规则作废——报出 0 个函数。
|
|
1687
|
+
// 把约束放进捕获点,匹配器就能连同 `$ident*` 的长度一起回溯出正确解。
|
|
1688
|
+
if (part === '$nameNotKeyword' && lang.keywords.has(t.value)) return null
|
|
1689
|
+
return matchPattern(tokens, start + 1, pattern, pi + 1, { ...cap, [part === '$nameNotKeyword' ? '$name' : part]: t.value }, lang, pairs)
|
|
1690
|
+
}
|
|
1691
|
+
if (t.value !== part) return null
|
|
1692
|
+
return matchPattern(tokens, start + 1, pattern, pi + 1, cap, lang, pairs)
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
/** 可选段的"跳"这一步:跳不动就原地返回,由调用方回溯到"不跳"的分支。 */
|
|
1696
|
+
function skipOptional(tokens, k, part, lang, pairs) {
|
|
1697
|
+
if (part === '$mods*') {
|
|
1698
|
+
let i = k
|
|
1699
|
+
while (tokens[i] !== undefined && tokens[i].type === 'ident' && lang.modifiers !== undefined && lang.modifiers.has(tokens[i].value)) i += 1
|
|
1700
|
+
return i
|
|
1701
|
+
}
|
|
1702
|
+
if (part === '$ident*') {
|
|
1703
|
+
// 只吃**非关键字**标识符:关键字是模式里的字面量,不该被这个段吞掉
|
|
1704
|
+
let i = k
|
|
1705
|
+
while (tokens[i] !== undefined && tokens[i].type === 'ident' && !lang.keywords.has(tokens[i].value)) i += 1
|
|
1706
|
+
return i
|
|
1707
|
+
}
|
|
1708
|
+
// $param*
|
|
1709
|
+
let i = k
|
|
1710
|
+
if (tokens[i] !== undefined && tokens[i].type === 'punct' && tokens[i].value === '(' && pairs.has(i)) i = pairs.get(i) + 1
|
|
1711
|
+
// 一个字符串也算可选段:HCL 的 `resource "aws_type" "name" {` 里 `$label` 捕走第一个标签后,
|
|
1712
|
+
// `$param*` 必须跳过第二个,否则块开符永远等不到(跨度只剩签名行)。
|
|
1713
|
+
if (tokens[i] !== undefined && tokens[i].type === 'string') i += 1
|
|
1714
|
+
const lt = tokens[i]
|
|
1715
|
+
if (lt !== undefined && lt.type === 'punct' && lt.value === '<') {
|
|
1716
|
+
const close = matchAngle(tokens, i)
|
|
1717
|
+
if (close !== -1) i = close + 1
|
|
1718
|
+
}
|
|
1719
|
+
return i
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
/**
|
|
1723
|
+
* 一条规则能否落在 tokens[i] 上,并给出声明起点。
|
|
1724
|
+
*
|
|
1725
|
+
* 关键区分:`$name` 是**捕获**,`$param` 是**跳过**。跨度只能由捕获与字面量决定——
|
|
1726
|
+
* 曾经把两者都当"消费",于是 `function describe(a, b) {` 的跨度从参数里的 `(` 起算,
|
|
1727
|
+
* 塌成一行;`const todos = items.map((item) => ...)` 的跨度从箭头函数参数起算,把
|
|
1728
|
+
* `for` 的块当成了变量作用域。这不是参数问题,是跨度语义错了。
|
|
1729
|
+
*
|
|
1730
|
+
* @returns {{cap, startTok, matchStart, end}|null} startTok 是名字所在 token;
|
|
1731
|
+
* matchStart 是规则**起点**——两者不同,且用途不能混(见 indexStructure 的唯一性闸门)
|
|
1732
|
+
*/
|
|
1733
|
+
function matchRule(tokens, i, rule, lang, pairs, lineAt) {
|
|
1734
|
+
const hit = matchPattern(tokens, i, rule.pattern, 0, {}, lang, pairs)
|
|
1735
|
+
if (hit === null) return null
|
|
1736
|
+
const { cap, end } = hit
|
|
1737
|
+
// 声明起点是**名字**所在的位置,不是模式起点:C 的 `static int *foo(` 起点在 `foo`。
|
|
1738
|
+
// 用名字 token 反查,避免把返回类型算进声明跨度。
|
|
1739
|
+
// `$label` 捕到的可能是**字符串** token(`resource "aws_instance"`),所以除了 ident
|
|
1740
|
+
// 也要认 string——但只认“值恰好等于捕获值”的那个(`$label` 不剥引号,见 matchPattern)。
|
|
1741
|
+
let startTok = i
|
|
1742
|
+
if (typeof cap.$name === 'string') {
|
|
1743
|
+
for (let k = i; k < end; k += 1) {
|
|
1744
|
+
if (tokens[k].value === cap.$name) {
|
|
1745
|
+
startTok = k
|
|
1746
|
+
break
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
const k = end
|
|
1751
|
+
if (rule.guard === 'notKeyword' && lang.keywords.has(cap.$name)) return null
|
|
1752
|
+
// 成员访问闸门:`foo.bar(...)` 里的 `bar` 是调用/属性,不是方法声明。
|
|
1753
|
+
// 没有这一道,`Object.assign({...})`、`ctx.sessionProjections.register({...})`
|
|
1754
|
+
// 会被认成方法声明——实测一个 197 行的文件里冒出 29 个假 method。
|
|
1755
|
+
if (rule.guard === 'memberName') {
|
|
1756
|
+
const prev = tokens[i - 1]
|
|
1757
|
+
if (prev !== undefined && prev.type === 'punct' && (prev.value === '.' || prev.value === '?.')) return null
|
|
1758
|
+
if (lang.keywords.has(cap.$name)) return null
|
|
1759
|
+
}
|
|
1760
|
+
// `body` 守卫:规则匹配完之后,下一个有效 token 必须是块体开符。
|
|
1761
|
+
// 这是 C 家族函数定义规则的判别器——`ret name(params) {` 与"调用 `foo(x);`"、
|
|
1762
|
+
// "声明 `int foo(int);`"在词法上唯一的硬差别就是参数表后面那个 `{`。
|
|
1763
|
+
// 名字不能是关键字这一条由模式里的 `$nameNotKeyword` 在**捕获点**判(见 matchPattern),
|
|
1764
|
+
// 放这里会因为无法回溯而误杀。
|
|
1765
|
+
if (rule.guard === 'body') {
|
|
1766
|
+
if (tokens[k] === undefined || tokens[k].value !== lang.bodyOpen) return null
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Erlang 的**子句**闸门:`beta(A) ->` 是函数子句,`-module(alpha).` 是属性。
|
|
1770
|
+
*
|
|
1771
|
+
* 两个判据:
|
|
1772
|
+
* 1. 名字前一个 token 不能是 `-`——属性规则(`['-', 'module', '$param']`)已经捕了属性,
|
|
1773
|
+
* 少了这一条,同一个 `-module(alpha)` 会被函数规则再命中一次并报出一个叫 `module`
|
|
1774
|
+
* 的假函数(名字来自属性词而不是真名字)。
|
|
1775
|
+
* 2. 参数表的**下一个** token 必须是 `->`:这是 Erlang 里函数子句与函数**调用**
|
|
1776
|
+
* (`foo(1),` 在体里到处出现)在词法上唯一的硬差别——与 C 家族的 `body` 守卫同理。
|
|
1777
|
+
*/
|
|
1778
|
+
/**
|
|
1779
|
+
* **第一列(layout)闸门**:仅当名字是该行**第一个** token 时才认。
|
|
1780
|
+
*
|
|
1781
|
+
* 为 Haskell 加的:那里的顶层绑定没有关键字可锚定(`beta a = a + 1` 与函数体内的
|
|
1782
|
+
* `a = a + 1` 在 token 流上完全同形),唯一的硬差别是 **layout 规则**——顶层绑定的名字在第一列。
|
|
1783
|
+
* 没有这一道,实测会报出假符号 `a@4-4`,而**假符号比没符号危险**:`target=a` 会改到参数身上。
|
|
1784
|
+
*/
|
|
1785
|
+
if (rule.guard === 'topLevel' && !clauseInitial(tokens, startTok, lineAt)) return null
|
|
1786
|
+
if (rule.guard === 'attributeClause') {
|
|
1787
|
+
// 判据要落在**规则起点**的前一个 token(`-`),不是名字的前一个:
|
|
1788
|
+
// `['-', 'define', '(', '$name', …]` 里名字前面是 `(`,拿它去比 `-` 永远不成立,
|
|
1789
|
+
// 于是同一个 `-define(ALPHA, 1)` 会被属性规则又命中一次(实测报出假符号 `define`)。
|
|
1790
|
+
const prev = tokens[i - 1]
|
|
1791
|
+
if (prev !== undefined && prev.type === 'punct' && prev.value === '-') return null
|
|
1792
|
+
const after = tokens[pairs.get(end - 1) + 1]
|
|
1793
|
+
if (after === undefined || after.value !== '->') return null
|
|
1794
|
+
}
|
|
1795
|
+
// 放最后,因为它与其它守卫互不依赖。
|
|
1796
|
+
if (looksLikeBinding(tokens, startTok, k, lang)) return null
|
|
1797
|
+
return { cap, startTok, matchStart: i, end: k }
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
/**
|
|
1801
|
+
* 这一处是不是"绑定"而非"声明"。
|
|
1802
|
+
*
|
|
1803
|
+
* 两个判据,缺一不可:
|
|
1804
|
+
* 1. **名字本身不能是关键字**。`export` 在 JS 的 `modifiers` 词表里(它是修饰符),
|
|
1805
|
+
* 但 `export function alpha` 里 `function` 已被"前有修饰符"闸门跳过,于是 `export`
|
|
1806
|
+
* 自己匹配上 `$mods* $name` 并把 **`function` 当成符号名**——符号表里出现两个 `alpha`,
|
|
1807
|
+
* 坐标完全相同,解析时报歧义、`target=alpha` 直接失效。
|
|
1808
|
+
* 2. **名字后面不能跟 `of` / `in`**,否则 `const item of list` 的 `item` 会被当成变量声明,
|
|
1809
|
+
* 跨度还覆盖整个循环体。
|
|
1810
|
+
*
|
|
1811
|
+
* 第一版只做了第 2 条,于是 `export function` / `public class`(凡是"修饰符+关键字"的写法)
|
|
1812
|
+
* 都会多出一个同名假符号。一个只查了后半段的守卫,比没有守卫更糟——它看起来在防护。
|
|
1813
|
+
*
|
|
1814
|
+
* @param tokens - token 流
|
|
1815
|
+
* @param nameTok - 名字所在下标
|
|
1816
|
+
* @param after - 名字之后的下标
|
|
1817
|
+
* @param lang - 语言配置(用它的关键字表判定,不新增名单)
|
|
1818
|
+
*/
|
|
1819
|
+
function looksLikeBinding(tokens, nameTok, after, lang) {
|
|
1820
|
+
const name = tokens[nameTok]
|
|
1821
|
+
if (name !== undefined && lang.keywords.has(name.value)) return true
|
|
1822
|
+
const t = tokens[after]
|
|
1823
|
+
return t !== undefined && t.type === 'ident' && lang.keywords.has(t.value) && (t.value === 'of' || t.value === 'in')
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
/**
|
|
1827
|
+
* 声明起点前不能是修饰符。
|
|
1828
|
+
*
|
|
1829
|
+
* `$mods*` 让"修饰符 + 关键字"的组合共用一条规则,代价是同一个声明能在多个位置匹配上:
|
|
1830
|
+
* `public static final class A` 会在 `class`、`static`、`public` 三处各命中一次,报出四个同名符号。
|
|
1831
|
+
* 这不是匹配错了,是**没有定义唯一性**——声明起点就是关键字本身,前面还有修饰符就说明
|
|
1832
|
+
* 命中的不是起点。用词表判定,因此对每门语言自动成立。
|
|
1833
|
+
*/
|
|
1834
|
+
function precededByModifier(tokens, i, lang) {
|
|
1835
|
+
const prev = tokens[i - 1]
|
|
1836
|
+
return prev !== undefined && prev.type === 'ident' && lang.modifiers !== undefined && lang.modifiers.has(prev.value)
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// ── 块体终结:跨度必须覆盖符号真实占据的整段 ────────────────────────────────
|
|
1840
|
+
|
|
1841
|
+
/** 一行的前导空白。 */
|
|
1842
|
+
const LEADING_SPACE = /^[ \t]*/
|
|
1843
|
+
|
|
1844
|
+
const leadingSpace = (line) => LEADING_SPACE.exec(line)[0]
|
|
1845
|
+
|
|
1846
|
+
/**
|
|
1847
|
+
* 两行的缩进关系:`inside`(更深)、`outside`(同级或更浅)、`ambiguous`(判不了)。
|
|
1848
|
+
*
|
|
1849
|
+
* 比的是**前导空白串**而不是格数:` ` 与 `\t` 谁更深取决于制表位宽度(Python 取 8),
|
|
1850
|
+
* 而这种"看着更深、实际同级"的差别正是缩进型语言最贵的静默错误。只有前缀关系成立才判得了;
|
|
1851
|
+
* 空格与制表符混用时返回 ambiguous,调用方据此 fail-closed,绝不猜。
|
|
1852
|
+
*/
|
|
1853
|
+
function indentRelation(inner, base) {
|
|
1854
|
+
if (inner === base) return 'outside'
|
|
1855
|
+
if (inner.startsWith(base)) return 'inside'
|
|
1856
|
+
if (base.startsWith(inner)) return 'outside'
|
|
1857
|
+
return 'ambiguous'
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
/**
|
|
1861
|
+
* 缩进型声明的头部末端:`def`/`class` 的头一定以 `:` 收尾。
|
|
1862
|
+
*
|
|
1863
|
+
* 必须先找这个 `:` 而不是直接拿 `)`,因为头部可以有续行(`def f(\n a) -> T:` 合法,
|
|
1864
|
+
* 且括号内的续行**可以顶格写**——那时按缩进判断会把头部后半截当成块体之外)。
|
|
1865
|
+
* 头部收尾符之后的 `=` / `;`(头部里不可能出现)说明已经走进块体,返回 -1 交给 fail-closed。
|
|
1866
|
+
* 但 `=` 对 OCaml/Haskell 恰恰**是**头部的收尾符,因此哪些 token 收尾由 `headerEnds` 给定,
|
|
1867
|
+
* 不写死成 `:`——写死会让那两门语言的每一个 `let … =` 都判成“头部没有收尾符”。
|
|
1868
|
+
*
|
|
1869
|
+
* @param headerEnds - 可作头部收尾符的 token 集合
|
|
1870
|
+
* @returns {number} 头部收尾符的 token 下标;找不到返回 -1
|
|
1871
|
+
*/
|
|
1872
|
+
function headerColon(tokens, fromTok, headerEnds, lineAt, pairs) {
|
|
1873
|
+
let depth = 0
|
|
1874
|
+
for (let k = fromTok + 1; k < tokens.length; k += 1) {
|
|
1875
|
+
const t = tokens[k]
|
|
1876
|
+
// 换行即停(括号**内**的续行不算,那时 depth > 0):
|
|
1877
|
+
// 少了这一条,扫描会越过本声明去撞**下一条语句**的 `=`,
|
|
1878
|
+
// 于是 `let alpha a =\n a + 1\n\nlet gamma = 2` 里 `alpha` 的"头部收尾符"变成了
|
|
1879
|
+
// `gamma` 那一行的 `=`(实测:跨度从 1-2 变成 1-4,把下一条声明也盖进来)。
|
|
1880
|
+
if (depth === 0 && k > fromTok + 1 && lineAt(t.start) > lineAt(tokens[k - 1].end)) return -1
|
|
1881
|
+
if (t.type !== 'punct') continue
|
|
1882
|
+
// **整个括号组一次跳过**:参数表里的 `:` 不是头部收尾符。
|
|
1883
|
+
// 少了这一条,Nim 的 `proc alpha(a: int): int =` 会在形参的 `a:` 上就"找到"收尾符
|
|
1884
|
+
// (实测:符号全部 spanComplete=false、一个符号都提不出来)。
|
|
1885
|
+
if (pairs !== undefined && pairs.has(k)) {
|
|
1886
|
+
const close = pairs.get(k)
|
|
1887
|
+
if (lineAt(tokens[close].end) > lineAt(t.start)) depth += 1
|
|
1888
|
+
k = close
|
|
1889
|
+
continue
|
|
1890
|
+
}
|
|
1891
|
+
if (t.value === '(' || t.value === '[' || t.value === '{') depth += 1
|
|
1892
|
+
else if (t.value === ')' || t.value === ']' || t.value === '}') depth -= 1
|
|
1893
|
+
else if (depth === 0 && headerEnds.has(t.value)) return k
|
|
1894
|
+
// `=` 是「块体开始」的信号,但它**只有在本语言里不是头部收尾符时**才是。
|
|
1895
|
+
// Nim 的 `proc f(a: int): int =` 里返回类型那段先出现 `:`,若在这里就认 `=`,
|
|
1896
|
+
// 判据会返回 -1(实测:Nim 的符号全部 spanComplete=false)。
|
|
1897
|
+
else if (t.value === ';' && depth === 0 && !headerEnds.has(';')) return -1
|
|
1898
|
+
else if (t.value === '=' && depth === 0 && !headerEnds.has('=')) return -1
|
|
1899
|
+
}
|
|
1900
|
+
return -1
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
/**
|
|
1904
|
+
* 缩进块体的末端(Python / Cython)。
|
|
1905
|
+
*
|
|
1906
|
+
* 判据只有一条:块体是**头部之后、第一条不比声明行更深的内容行之前**的整段。
|
|
1907
|
+
* 走 token 流而不是行文本,是为了让空行、注释行、多行字符串自然落到正确的一侧:
|
|
1908
|
+
* · 空行与纯注释行不产 token,因此不会被收进跨度(收进去等于静默删注释);
|
|
1909
|
+
* · 三引号字符串是一个 token,跟着它走到**结束行**,不会被字符串里顶格的文本误判成块体结束。
|
|
1910
|
+
*
|
|
1911
|
+
* @returns {{endTok: number, note: string}} 判不了时 endTok = -1,note 带上实际值(行号与两种缩进)
|
|
1912
|
+
*/
|
|
1913
|
+
function indentBlockEnd(tokens, lines, fromTok, baseLine, baseIndent, lineAt, headerEnds, pairs) {
|
|
1914
|
+
const colon = headerColon(tokens, fromTok, headerEnds, lineAt, pairs)
|
|
1915
|
+
if (colon === -1) {
|
|
1916
|
+
return {
|
|
1917
|
+
endTok: -1,
|
|
1918
|
+
note: `line ${baseLine}: no ${[...headerEnds].map((h) => JSON.stringify(h)).join('/')} terminates this declaration's header, so its block cannot be delimited`,
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
let last = colon
|
|
1922
|
+
const headerLine = lineAt(tokens[colon].end)
|
|
1923
|
+
/**
|
|
1924
|
+
* 头部收尾符就是**最后一个 token**:声明是单行的(`main = print 1`、`let gamma = 2`)。
|
|
1925
|
+
* 那时跨度就是这一行——没有块体,也没有"判不了"的成分。
|
|
1926
|
+
*
|
|
1927
|
+
* 这一条不能省:循环会因为越界直接结束,`last` 停在收尾符上,而调用方**无法区分**
|
|
1928
|
+
* “真的只有一行”与“循环没走完”。实测(Haskell 的 `main =`、OCaml 的 `let gamma =`)
|
|
1929
|
+
* 两种语言的文件末尾绑定都因此被标成 `spanComplete: false` 并拒绝 `target=`。
|
|
1930
|
+
*/
|
|
1931
|
+
if (colon === tokens.length - 1) return { endTok: colon, note: '' }
|
|
1932
|
+
for (let k = colon + 1; k < tokens.length; k += 1) {
|
|
1933
|
+
const startLine = lineAt(tokens[k].start)
|
|
1934
|
+
if (startLine <= headerLine) {
|
|
1935
|
+
// 头部的续行(`def f(\n a):`)不是块体,但必须落在跨度里
|
|
1936
|
+
last = k
|
|
1937
|
+
continue
|
|
1938
|
+
}
|
|
1939
|
+
const indent = leadingSpace(lines[startLine - 1] ?? '')
|
|
1940
|
+
const rel = indentRelation(indent, baseIndent)
|
|
1941
|
+
if (rel === 'inside') {
|
|
1942
|
+
last = k
|
|
1943
|
+
continue
|
|
1944
|
+
}
|
|
1945
|
+
if (rel === 'outside') break
|
|
1946
|
+
return {
|
|
1947
|
+
endTok: -1,
|
|
1948
|
+
note: `line ${startLine} is indented ${JSON.stringify(indent)} while the declaration line ${baseLine} is indented ${JSON.stringify(baseIndent)} — tabs and spaces cannot be compared, so the block end is not determinable`,
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
return { endTok: last, note: '' }
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
/**
|
|
1955
|
+
* 后置修饰符判据:`if`/`unless`/`while`/`until` 出现在这些 token 之后时,它开的是一个块
|
|
1956
|
+
* (`x = if c … end`、`foo(if c then … end)`);出现在别处(标识符/数字/字符串/`)` 之后)则是
|
|
1957
|
+
* **后置修饰符**(`puts x if y`),不开块。
|
|
1958
|
+
*
|
|
1959
|
+
* 与 `REGEX_PRECEDING_KEYWORDS` 同类:这是**词法**判据表,不是关键字表,因此不进语言的 `keywords`。
|
|
1960
|
+
* 表里少一项只会让跨度偏短,而那会被 `closerBlockEnd` 的缩进校验拦住 → fail-closed,方向是安全的。
|
|
1961
|
+
*/
|
|
1962
|
+
const CLAUSE_BOUNDARY = new Set([
|
|
1963
|
+
';', 'then', 'else', 'elsif', 'do', '=', '(', '[', '{', ',', ':', '=>', '?',
|
|
1964
|
+
'&&', '||', 'and', 'or', 'not', '!',
|
|
1965
|
+
'+=', '-=', '*=', '/=', '%=', '**=', '||=', '&&=', '<<=', '>>=', '|=', '&=',
|
|
1966
|
+
])
|
|
1967
|
+
|
|
1968
|
+
/** 这个关键字是否落在子句起始位置(行首,或紧跟一个不能结束表达式的 token)。 */
|
|
1969
|
+
function clauseInitial(tokens, k, lineAt) {
|
|
1970
|
+
const prev = tokens[k - 1]
|
|
1971
|
+
if (prev === undefined) return true
|
|
1972
|
+
if (lineAt(prev.end) !== lineAt(tokens[k].start)) return true
|
|
1973
|
+
return CLAUSE_BOUNDARY.has(prev.value)
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
/** 成员访问闸门:`x.class` 里的 `class` 是方法名,不是开块词。 */
|
|
1977
|
+
function isMemberName(tokens, k) {
|
|
1978
|
+
const prev = tokens[k - 1]
|
|
1979
|
+
return prev !== undefined && prev.type === 'punct' && (prev.value === '.' || prev.value === '&.' || prev.value === '::')
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
/**
|
|
1983
|
+
* 这一处的 `<<` 是不是 heredoc 引入符(`<<~TAG` / `<<-TAG` / `<<"TAG"` / `<<TAG`)。
|
|
1984
|
+
*
|
|
1985
|
+
* 词法器**不认识 heredoc**:它的正文会被当成代码扫成 token,于是正文里顶格的一行 `end`
|
|
1986
|
+
* 会被当成真收尾词,把跨度截在正文中间——静默改错地方。判据取"紧贴 + 位于行尾":
|
|
1987
|
+
* 真的 heredoc 引入符后面紧跟标签并立即换行。代价是 `list <<ITEM` 这种紧贴写法会被误判,
|
|
1988
|
+
* 后果只是该符号拒绝 `target=`(安全方向)——这是刻意接受的过近似。
|
|
1989
|
+
*/
|
|
1990
|
+
function heredocAt(tokens, k, lineAt) {
|
|
1991
|
+
const t = tokens[k]
|
|
1992
|
+
const next = tokens[k + 1]
|
|
1993
|
+
if (next === undefined || next.start !== t.end) return false
|
|
1994
|
+
let end = k + 1
|
|
1995
|
+
if (next.value === '~' || next.value === '-') {
|
|
1996
|
+
// `<<~TAG` / `<<-TAG`:前缀之后才是标签
|
|
1997
|
+
const tag = tokens[k + 2]
|
|
1998
|
+
if (tag === undefined || tag.start !== next.end) return false
|
|
1999
|
+
end = k + 2
|
|
2000
|
+
}
|
|
2001
|
+
const tag = tokens[end]
|
|
2002
|
+
const named = tag.type === 'ident' && /^[A-Z_]/.test(tag.value)
|
|
2003
|
+
const quoted = tag.type === 'string' && (tag.value.startsWith('"') || tag.value.startsWith("'"))
|
|
2004
|
+
if (!named && !quoted) return false
|
|
2005
|
+
// heredoc 的正文从**下一行**开始:引入符这一行的标签之后不该再有 token
|
|
2006
|
+
const after = tokens[end + 1]
|
|
2007
|
+
return after === undefined || lineAt(after.start) > lineAt(t.start)
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
/**
|
|
2011
|
+
* `end` 收尾块体的末端(Ruby / Lua)。
|
|
2012
|
+
*
|
|
2013
|
+
* 按 `openers` 与 `closer` 配对计数,深度回到 0 的那个收尾词就是末端。两道校验把"数错了"
|
|
2014
|
+
* 变成 fail-closed 而不是偏短的跨度:
|
|
2015
|
+
* 1. **缩进校验**:Ruby/Lua 的收尾词与它的开块词对齐,因此收尾词所在行的前导空白必须与声明行相同。
|
|
2016
|
+
* 正则里的 `end`(`/end/`)会在这里露馅。
|
|
2017
|
+
* 2. **heredoc 校验**:见 {@link heredocAt}。
|
|
2018
|
+
*
|
|
2019
|
+
* @returns {{endTok: number, note: string}} 判不了时 endTok = -1,note 带上实际值
|
|
2020
|
+
*/
|
|
2021
|
+
function closerBlockEnd(tokens, startTok, fromTok, spec, lineAt, lines) {
|
|
2022
|
+
const baseLine = lineAt(tokens[startTok].start)
|
|
2023
|
+
const baseIndent = leadingSpace(lines[baseLine - 1] ?? '')
|
|
2024
|
+
let depth = 1
|
|
2025
|
+
let loopLine = -1
|
|
2026
|
+
for (let k = fromTok + 1; k < tokens.length; k += 1) {
|
|
2027
|
+
const t = tokens[k]
|
|
2028
|
+
const line = lineAt(t.start)
|
|
2029
|
+
if (t.type === 'punct' && t.value === '<<' && heredocAt(tokens, k, lineAt)) {
|
|
2030
|
+
return { endTok: -1, note: `line ${line}: heredoc is not understood by the lexer, so the block end is not determinable` }
|
|
2031
|
+
}
|
|
2032
|
+
if (t.type !== 'ident') continue
|
|
2033
|
+
if (spec.markers.has(t.value) && loopLine !== -1) {
|
|
2034
|
+
// 循环体的引导词:`for i=1,10 do` 只有一个块,这一处的 `do` 不额外计数
|
|
2035
|
+
loopLine = -1
|
|
2036
|
+
continue
|
|
2037
|
+
}
|
|
2038
|
+
if (loopLine !== -1 && line !== loopLine) loopLine = -1
|
|
2039
|
+
if (spec.openers.has(t.value)) {
|
|
2040
|
+
if (spec.modifiers.has(t.value) && !clauseInitial(tokens, k, lineAt)) continue
|
|
2041
|
+
if (isMemberName(tokens, k)) continue
|
|
2042
|
+
depth += 1
|
|
2043
|
+
if (spec.loopOpeners.has(t.value)) loopLine = line
|
|
2044
|
+
continue
|
|
2045
|
+
}
|
|
2046
|
+
if (t.value === spec.closer || (spec.altCloser !== undefined && t.value === spec.altCloser)) {
|
|
2047
|
+
depth -= 1
|
|
2048
|
+
if (depth > 0) continue
|
|
2049
|
+
const closeIndent = leadingSpace(lines[line - 1] ?? '')
|
|
2050
|
+
if (closeIndent !== baseIndent) {
|
|
2051
|
+
return {
|
|
2052
|
+
endTok: -1,
|
|
2053
|
+
note: `the "${t.value}" at line ${line} that would close the declaration at line ${baseLine} is indented ${JSON.stringify(closeIndent)} instead of ${JSON.stringify(baseIndent)}`,
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
return { endTok: k, note: '' }
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
return { endTok: -1, note: `no "${spec.closer}" closes the declaration at line ${baseLine} (depth ${depth} at end of file)` }
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
/**
|
|
2063
|
+
* 语句终结符收尾的块体末端(SQL 的 `;`、Erlang 的 `.`、Vim 的 `endfunction`)。
|
|
2064
|
+
*
|
|
2065
|
+
* 三条停止条件,都取自本文件的既有概念:
|
|
2066
|
+
* 1. **终结符**:`terminators` 里的 token(SQL 的 `;`、Erlang 的 `.`)或关键字
|
|
2067
|
+
* (Vim 的 `endfunction`)。关键字按小写比对——Vim 允许 `EndFunction`,
|
|
2068
|
+
* 只比原样会把跨度截短(静默)。
|
|
2069
|
+
* 2. **换行**:行尾停下的 token 是**上一行**的末尾,因此终结符在下一行时
|
|
2070
|
+
* (`CREATE TABLE alpha (\n beta INTEGER\n);`)照常能走到它;一行里没找到终结符时
|
|
2071
|
+
* 宣告终结的那一位就是块闭符(HCL 的 `}`)——跳过 `{ … }` 整层时记下的正是它。
|
|
2072
|
+
* 3. **括号组整层跨过**:`SELECT COUNT(*) AS n` 里的 `(` `)` 一次跳完,
|
|
2073
|
+
* 于是“终结符在括号外”只需看括号**之后**那个 token,不必维护深度变量。
|
|
2074
|
+
*
|
|
2075
|
+
* 跳层必须把它落在**组尾**上(`skippedEnd`),不能只是把游标移过去:块体本身就是那个组时
|
|
2076
|
+
* (HCL 的 `variable "x" { … }`),组尾就是声明的最后一位,丢掉它会让跨度只剩签名行。
|
|
2077
|
+
*
|
|
2078
|
+
* @returns {{endTok: number, note: string}} 找不到任何收尾时 endTok = -1(fail-closed)。
|
|
2079
|
+
* 不退回“只有签名行”:那正是本文件最贵的那个静默失效。
|
|
2080
|
+
*
|
|
2081
|
+
* ── 为什么要跳表(这段曾经的 O(n²) 退化)────────────────────────────────────
|
|
2082
|
+
* 三条停止条件里,第 1、3 条在**整份文件只有一行**、且语言是终结符型(SQL / Erlang / Vim)时
|
|
2083
|
+
* 永不成立:没有换行,语料里也没有终结符 ⇒ 每个声明都一路扫到 token 流末尾 ⇒ 全文件 O(n²)。
|
|
2084
|
+
* 退化形状(`CREATE TABLE` 声明并成一行、一个 `;` 都没有;1k/2k/4k/8k)中位数实测:
|
|
2085
|
+
* 改前 76.9 / 277.1 / 1145.8 / 5709.9 ms(每翻一倍 ×3.6~×5.0),改后 14.6 / 26.0 / 51.6 / 87.2 ms
|
|
2086
|
+
*(×1.7~×2.0),末档提速 ×65.5。读数时机内另有并行负载,判据取比值不取绝对值。
|
|
2087
|
+
*
|
|
2088
|
+
* 改法 = 两张长度 n 的 `Int32Array` 后缀最小值表(每次 `buildStructure` **只建一次**,O(n)):
|
|
2089
|
+
* · `nextStop[j]` = 最小的 `i ≥ j`,使 `isTerminatorToken(i)` 或(`i` 不是括号开符且 `nl(i)`);
|
|
2090
|
+
* · `nextPairOpen[j]` = 最小的 `i ≥ j` 且 `i` 是配平括号组的开符。
|
|
2091
|
+
* 同一个 k 上的判定次序照抄原循环(终结符 → 括号跳层 → 换行),所以「既是开符又换行」的位置
|
|
2092
|
+
* **不进** `nextStop`;`nl(i)` 就是原来的 `lineAt(tokens[i].start) > lineAt(tokens[i-1].end)`(`i ≥ 1`)
|
|
2093
|
+
* —— 跳层之后 `i - 1` 正好是组尾,与原代码逐字一致。于是每个 k 只做一次 O(1) 的读表:
|
|
2094
|
+
* 两张表里先到的那个决定「返回」还是「整层跳过去」,而跳层次数被括号层数封顶 ⇒ 每份声明 O(跳层数)。
|
|
2095
|
+
*
|
|
2096
|
+
* 首 token(`fromTok + 1`)不适用换行那条(原循环的 `k > fromTok + 1` 守卫),单独处理一次。
|
|
2097
|
+
* 换行命中返回的仍是 `skippedEnd`(没有跳层时退回 `s - 1`),与逐 token 扫描逐字相同。
|
|
2098
|
+
*/
|
|
2099
|
+
/** 终结符 token 判据:`punct` 比原值、`ident` 比小写(其余 token 类型一律不是)。 */
|
|
2100
|
+
function isTerminatorToken(t, terminators) {
|
|
2101
|
+
if (t.type === 'punct') return terminators.has(t.value)
|
|
2102
|
+
if (t.type === 'ident') return terminators.has(t.value.toLowerCase())
|
|
2103
|
+
return false
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
/**
|
|
2107
|
+
* {@link terminatorBlockEnd} 用的两张后缀最小值表。判定优先级与逐 token 扫描同一个 k 上的次序相同。
|
|
2108
|
+
* 只在该语言真的走终结符风格时才会被调用。
|
|
2109
|
+
*/
|
|
2110
|
+
function terminatorScan(tokens, pairs, lineAt, terminators) {
|
|
2111
|
+
const n = tokens.length
|
|
2112
|
+
const nextStop = new Int32Array(n)
|
|
2113
|
+
const nextPairOpen = new Int32Array(n)
|
|
2114
|
+
/**
|
|
2115
|
+
* `chainEnd[j]` = 从 `j` 起**只做括号跳层**能走到的最后一个开符(没有就是 -1)。
|
|
2116
|
+
*
|
|
2117
|
+
* 为什么要它:一个停止条件都找不到时,原循环会把能碰到的顶层括号组逐个跨过去,最后停在
|
|
2118
|
+
* **最后一个**组尾上(不是第一个)——逐组跳是 O(组数),而这条链的尾巴可以在建表时一次算完。
|
|
2119
|
+
* 这一支正是退化形状(整份文件一行、无终结符,而声明带括号参数表)里每一份声明都要走的那条路。
|
|
2120
|
+
*/
|
|
2121
|
+
const chainEnd = new Int32Array(n)
|
|
2122
|
+
const chainEndFrom = new Int32Array(n)
|
|
2123
|
+
let stop = -1
|
|
2124
|
+
let open = -1
|
|
2125
|
+
for (let i = n - 1; i >= 0; i -= 1) {
|
|
2126
|
+
const t = tokens[i]
|
|
2127
|
+
const isOpen = t.type === 'punct' && pairs.has(i)
|
|
2128
|
+
if (isOpen) {
|
|
2129
|
+
open = i
|
|
2130
|
+
// 跳过本组之后的第一个开符若已算过,就接上它的链尾(下标严格更大 ⇒ 从右往左一定算过了)
|
|
2131
|
+
const after = pairs.get(i) + 1
|
|
2132
|
+
chainEnd[i] = after < n && nextPairOpen[after] !== -1 ? chainEnd[nextPairOpen[after]] : i
|
|
2133
|
+
}
|
|
2134
|
+
// 终结符优先于括号跳层,括号跳层优先于换行 —— 与原循环在同一个 k 上的判定次序一一对应
|
|
2135
|
+
if (isTerminatorToken(t, terminators) || (!isOpen && i >= 1 && lineAt(t.start) > lineAt(tokens[i - 1].end))) {
|
|
2136
|
+
stop = i
|
|
2137
|
+
}
|
|
2138
|
+
nextStop[i] = stop
|
|
2139
|
+
nextPairOpen[i] = open
|
|
2140
|
+
chainEndFrom[i] = open === -1 ? -1 : chainEnd[open]
|
|
2141
|
+
}
|
|
2142
|
+
return { nextStop, nextPairOpen, chainEndFrom }
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
/**
|
|
2146
|
+
* **每次建索引只建一次**的跳表:第一次真的用到时才建(非终结符型语言一个字都不付),
|
|
2147
|
+
* 之后同一份内容的所有声明共用同一张表 —— 这正是把 O(n²) 拉回 O(n) 的那一步。
|
|
2148
|
+
*/
|
|
2149
|
+
function lazyTerminatorScan(tokens, pairs, lineAt) {
|
|
2150
|
+
let table = null
|
|
2151
|
+
return (terminators) => {
|
|
2152
|
+
if (table === null) table = terminatorScan(tokens, pairs, lineAt, terminators)
|
|
2153
|
+
return table
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
/**
|
|
2158
|
+
* 语句终结符收尾的块体末端(SQL 的 `;`、Erlang 的 `.`、Vim 的 `endfunction`)。
|
|
2159
|
+
* 三条停止条件、跳层的落点、以及为什么要跳表,写在上面那一段里(那段就是这个函数的文档,
|
|
2160
|
+
* 判据与跳表分别落在 {@link isTerminatorToken} 与 {@link terminatorScan} 上)。
|
|
2161
|
+
*/
|
|
2162
|
+
function terminatorBlockEnd(tokens, pairs, fromTok, spec, lineAt, scan) {
|
|
2163
|
+
let skippedEnd = -1
|
|
2164
|
+
let k = fromTok + 1
|
|
2165
|
+
if (k >= tokens.length) return { endTok: -1, note: '' }
|
|
2166
|
+
// 首 token 不适用「换行」那条(原循环的 `k > fromTok + 1` 守卫):单独处理,其后全部走表。
|
|
2167
|
+
const first = tokens[k]
|
|
2168
|
+
if (isTerminatorToken(first, spec.terminators)) return { endTok: k, note: '' }
|
|
2169
|
+
if (first.type === 'punct' && pairs.has(k)) {
|
|
2170
|
+
skippedEnd = pairs.get(k)
|
|
2171
|
+
k = skippedEnd + 1
|
|
2172
|
+
} else k += 1
|
|
2173
|
+
const { nextStop, nextPairOpen, chainEndFrom } = scan
|
|
2174
|
+
while (k < tokens.length) {
|
|
2175
|
+
const s = nextStop[k]
|
|
2176
|
+
const o = nextPairOpen[k]
|
|
2177
|
+
if (s !== -1 && (o === -1 || s < o)) {
|
|
2178
|
+
// stop 位置一定不是括号开符(建表时就排除了),所以 `s === o` 不可能出现
|
|
2179
|
+
if (isTerminatorToken(tokens[s], spec.terminators)) return { endTok: s, note: '' }
|
|
2180
|
+
return { endTok: skippedEnd === -1 ? s - 1 : skippedEnd, note: '' }
|
|
2181
|
+
}
|
|
2182
|
+
if (o === -1) return { endTok: skippedEnd, note: '' }
|
|
2183
|
+
// 这一段里一个停止条件都没有:原循环会一路跨到**最后一个**组尾 ⇒ 用链尾表一次到位(O(1))
|
|
2184
|
+
if (s === -1) return { endTok: pairs.get(chainEndFrom[k]), note: '' }
|
|
2185
|
+
skippedEnd = pairs.get(o)
|
|
2186
|
+
k = skippedEnd + 1
|
|
2187
|
+
}
|
|
2188
|
+
return { endTok: skippedEnd, note: '' }
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
/**
|
|
2192
|
+
* 头部扫描:从规则末端走到块开符(或走到语句边界)。
|
|
2193
|
+
*
|
|
2194
|
+
* 原实现是一个 `endTok + 6` 的 token 窗口,窗口够不到就退回"只有签名行":
|
|
2195
|
+
* `const f = (a, b) => { … }` 的块开符在第 7 个 token,于是 `target=f` 只换签名行、
|
|
2196
|
+
* 函数体原样留下,而 `spanComplete` 仍然是 true——静默改错地方。
|
|
2197
|
+
* 现在**长度不再是判据**,判据是结构:
|
|
2198
|
+
* 1. 配平括号组(参数表、泛型、调用实参)**整层跨过** ⇒ 多长都只是一步
|
|
2199
|
+
* 2. 深度 0 的块开符 `{` 就是块体;`[` 也算(`const X = [ … ]` 的跨度要盖住整个字面量)
|
|
2200
|
+
* 3. `;` 与 `}` 是语句/块边界 ⇒ 声明到此结束(没有块体)
|
|
2201
|
+
* 4. 换行处看下一 token 能不能续行(见 {@link headerContinues}),不能就是新语句开始
|
|
2202
|
+
*
|
|
2203
|
+
* 返回的 `lastTok` 是**属于这个声明的最后一个 token**:有块体时是闭符,没有块体时是语句末尾。
|
|
2204
|
+
* 因此"没有块体"不再等于"只有签名行"——`const ok = first\n && second` 的跨度是两行。
|
|
2205
|
+
*
|
|
2206
|
+
* 返回的 `uncertainTok` 不是"兜底",是把"判不了"如实报出来:停在了一个既不能续行、
|
|
2207
|
+
* 也不能起新声明的 token 上(例如某个续行关键字漏进了配置表)。那种时候宁可拒绝,
|
|
2208
|
+
* 不交出一个偏短的跨度。
|
|
2209
|
+
*
|
|
2210
|
+
* @returns {{bodyOpen: number, lastTok: number, sawBinding: boolean, uncertainTok: number}}
|
|
2211
|
+
* bodyOpen = -1 表示没有块体;uncertainTok = -1 表示这个判断是确定的
|
|
2212
|
+
*/
|
|
2213
|
+
function scanBodyOpen(tokens, pairs, endTok, lang, lineAt, kind) {
|
|
2214
|
+
const open = lang.bodyOpen
|
|
2215
|
+
const clauses = lang.headerKeywords ?? NO_HEADER_KEYWORDS
|
|
2216
|
+
let last = endTok
|
|
2217
|
+
let sawBinding = false
|
|
2218
|
+
let k = endTok + 1
|
|
2219
|
+
while (k < tokens.length) {
|
|
2220
|
+
const t = tokens[k]
|
|
2221
|
+
const prev = tokens[k - 1]
|
|
2222
|
+
if (t.type === 'punct' && (t.value === ';' || t.value === '}')) break
|
|
2223
|
+
/**
|
|
2224
|
+
* 块开符判定:`bodyOpen` 本身,加上**配置声明的**其它块开符(`[$ … ]` 字面量体),
|
|
2225
|
+
* 再加上 `bodyOpens` 里显式列出的字符。
|
|
2226
|
+
*
|
|
2227
|
+
* 这里曾经无条件把 `(` 也算作块开符(为了让 Lisp 的 `(defn name [args] …)` 成立),
|
|
2228
|
+
* 那是**回归**:`fun alpha(a: Int): Int {` 的参数表就是一个配平的 `(`,
|
|
2229
|
+
* 于是跨度从函数定义塌到参数表那一行(实测 Kotlin/KTS 的 `target=` 从 1-3 变成 1-1,
|
|
2230
|
+
* 且 `spanComplete` 还是 true —— 静默改错地方,正是本文件最贵的那个失效)。
|
|
2231
|
+
* 判据因此回到配置表:不写 `bodyOpens` 的语言拿不到 `(`。
|
|
2232
|
+
*/
|
|
2233
|
+
const blockOpens = lang.bodyOpens ?? DEFAULT_BODY_OPENS
|
|
2234
|
+
if (t.type === 'punct' && (t.value === open || blockOpens.has(t.value))) {
|
|
2235
|
+
if (pairs.has(k)) return { bodyOpen: k, lastTok: pairs.get(k), sawBinding, uncertainTok: -1 }
|
|
2236
|
+
/**
|
|
2237
|
+
* 块开符出现了,却没有配平 ⇒ 配平表对这个文件不可信(典型起因:字符串/转义的扫描
|
|
2238
|
+
* 吃掉了一个闭符)。此时继续往前走会拿**内层的**块当本声明的块体(实测
|
|
2239
|
+
* `install.ps1` 的 `ForEach-Object { … }` 被当成了 `Get-RelFiles` 的体)。
|
|
2240
|
+
* 宁可拒绝,不赌。
|
|
2241
|
+
*/
|
|
2242
|
+
return { bodyOpen: -1, lastTok: last, sawBinding, uncertainTok: k }
|
|
2243
|
+
}
|
|
2244
|
+
if (kind === 'variable' && t.type === 'punct' && t.value === '=>') sawBinding = true
|
|
2245
|
+
if (kind === 'variable' && t.type === 'ident' && t.value === 'function') sawBinding = true
|
|
2246
|
+
if (t.type === 'punct' && pairs.has(k)) {
|
|
2247
|
+
// 跨过一整层:参数表 / 泛型 / 调用实参无论多长都算一步
|
|
2248
|
+
last = pairs.get(k)
|
|
2249
|
+
k = last + 1
|
|
2250
|
+
continue
|
|
2251
|
+
}
|
|
2252
|
+
if (lineAt(t.start) > lineAt(prev.end) && !headerContinues(tokens, k, prev, clauses)) {
|
|
2253
|
+
/**
|
|
2254
|
+
* 停在新语句的开头。变量声明到这里就结束了(表达式已经完整)。
|
|
2255
|
+
* 其它种类(function / class / rpc…)只有当下一个 token 确实能**起一个新声明**时,
|
|
2256
|
+
* 才敢断言"这个声明没有块体";停在别的关键字上说明头部可能被截早了——fail-closed。
|
|
2257
|
+
*/
|
|
2258
|
+
if (kind !== 'variable' && !declarationStartTokens(lang).has(t.value) && k < tokens.length - 1) {
|
|
2259
|
+
return { bodyOpen: -1, lastTok: last, sawBinding, uncertainTok: k }
|
|
2260
|
+
}
|
|
2261
|
+
// 停在**最后**一个 token 上 = 文件到此为止:不是"判不了",是"这就是末尾"。
|
|
2262
|
+
// 少了这个区分,块括号语言的文件末尾声明(`main = print 1`)会被标成跨度不完整。
|
|
2263
|
+
break
|
|
2264
|
+
}
|
|
2265
|
+
last = k
|
|
2266
|
+
k += 1
|
|
2267
|
+
}
|
|
2268
|
+
return { bodyOpen: -1, lastTok: last, sawBinding, uncertainTok: -1 }
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
/**
|
|
2272
|
+
* 一个语言的"声明起头" token:规则模式里的第一个字面量(`function` / `class` / `rpc`…)
|
|
2273
|
+
* 加上修饰符词表(`export` / `public`…)。
|
|
2274
|
+
*
|
|
2275
|
+
* 用于判断换行处那一行是**新声明**还是本声明的续行。从配置表派生,不手写名单——
|
|
2276
|
+
* 手写名单必然与规则脱节,而脱节是静默的(本文件反复踩过的那个坑)。
|
|
2277
|
+
* 按语言对象缓存:索引一份文件时每个声明都会问,不缓存就是反复遍历规则表。
|
|
2278
|
+
*/
|
|
2279
|
+
const DECLARATION_STARTS = new WeakMap()
|
|
2280
|
+
|
|
2281
|
+
function declarationStartTokens(lang) {
|
|
2282
|
+
let starts = DECLARATION_STARTS.get(lang)
|
|
2283
|
+
if (starts === undefined) {
|
|
2284
|
+
starts = new Set(lang.modifiers ?? [])
|
|
2285
|
+
for (const rule of lang.rules ?? []) {
|
|
2286
|
+
for (const part of rule.pattern) {
|
|
2287
|
+
if (typeof part === 'string' && !part.startsWith('$')) {
|
|
2288
|
+
starts.add(part)
|
|
2289
|
+
break
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
DECLARATION_STARTS.set(lang, starts)
|
|
2294
|
+
}
|
|
2295
|
+
return starts
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
/**
|
|
2299
|
+
* 换行处:下一个 token 还在声明头部里吗?
|
|
2300
|
+
*
|
|
2301
|
+
* 两层判据,回答的都是"上一行结束的子句/表达式还有没有下文":
|
|
2302
|
+
* · 上一 token **期待一个表达式**(`=`、`,`、`=>`、`+`…,复用 {@link tokenExpectsExpression})
|
|
2303
|
+
* 或上一 token 是子句关键字(`extends` / `implements` / `where`…)⇒ 续行
|
|
2304
|
+
* · 下一 token 是子句关键字,或是一个**起不了句的标点**(`&&`、`.`、`,`、`=`…)⇒ 续行
|
|
2305
|
+
* · 其余(标识符、关键字、字面量)⇒ 新语句开始,声明到此结束
|
|
2306
|
+
*
|
|
2307
|
+
* 最后一条是保守方向:只在"上一 token 能收尾"且"下一 token 像新语句的开头"时才断开。
|
|
2308
|
+
* 断错的代价是跨度偏短(`class A extends B\n implements C {` 的 `implements` 若不在配置表里),
|
|
2309
|
+
* 因此子句关键字与其它语言差异一样进配置表。
|
|
2310
|
+
*
|
|
2311
|
+
* @returns 真表示下一 token 仍属于声明头部
|
|
2312
|
+
*/
|
|
2313
|
+
function headerContinues(tokens, k, prev, clauses) {
|
|
2314
|
+
if (tokenExpectsExpression(prev) || clauses.has(prev.value)) return true
|
|
2315
|
+
const t = tokens[k]
|
|
2316
|
+
if (clauses.has(t.value)) return true
|
|
2317
|
+
return t.type === 'punct' && t.value !== ')' && t.value !== ']' && t.value !== '}'
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
/**
|
|
2321
|
+
* 声明跨度与**声明种类**:从规则起点到其块体(`{...}`)的配平闭括号,没有块体则到规则末端。
|
|
2322
|
+
*
|
|
2323
|
+
* 块体边界是语言相关的真差异(花括号 / Python 的缩进 / Ruby 的 `end`),因此它进配置表
|
|
2324
|
+
* (`bodyOpen` 或 `blockEnd`),扫描器里不写语言分支。
|
|
2325
|
+
*
|
|
2326
|
+
* **跨度必须覆盖符号真实占据的整段**:拿不到完整跨度时不是退化成签名行,而是标记
|
|
2327
|
+
* `spanComplete: false` —— 调用方(`locate`,进而 `aura_edit`)据此刻拒绝 `target=` 替换。
|
|
2328
|
+
* 理由是实测的:偏短的跨度会让"只换签名行"这件事**静默**发生,落盘之后语法检查毫无反应。
|
|
2329
|
+
*
|
|
2330
|
+
* 这里还要分辨一件事:`const describe = (x) => { ... }` 是**函数绑定**,不是普通变量。
|
|
2331
|
+
* 判据写在跨度计算里,因为它与"块体从哪开始"是同一个决策——都必须先跨过赋值右侧。
|
|
2332
|
+
* 用独立的第二条规则去做会与变量规则重复命中同一个位置,靠规则顺序决定谁赢,
|
|
2333
|
+
* 那是在用"顺序"掩盖语义,典型补丁。
|
|
2334
|
+
*/
|
|
2335
|
+
function declarationSpan(tokens, pairs, startTok, endTok, lang, lineAt, kind, lines, termScan) {
|
|
2336
|
+
const open = lang.bodyOpen
|
|
2337
|
+
let bodyOpen = null
|
|
2338
|
+
let binding = kind
|
|
2339
|
+
let last = endTok
|
|
2340
|
+
let spanComplete = true
|
|
2341
|
+
let spanNote = ''
|
|
2342
|
+
|
|
2343
|
+
if (open !== null) {
|
|
2344
|
+
/**
|
|
2345
|
+
* 规则末端**就是**块开符的形态(`name() {`、`int f() {`:模式把 `{` 写进去了)。
|
|
2346
|
+
* 那时块体从它开始,不必再扫——扫反而会撞上块体内部的第一个 `[` / `{`:
|
|
2347
|
+
* 实测 `alpha() { if [ -n "$x" ]; then … }` 的跨度会截在 `[ … ]` 那一行。
|
|
2348
|
+
*/
|
|
2349
|
+
const ruleEndsAtBody =
|
|
2350
|
+
tokens[endTok] !== undefined && tokens[endTok].type === 'punct' && tokens[endTok].value === open && pairs.has(endTok)
|
|
2351
|
+
const scan = ruleEndsAtBody
|
|
2352
|
+
? { bodyOpen: endTok, lastTok: pairs.get(endTok), sawBinding: false, uncertainTok: -1 }
|
|
2353
|
+
: scanBodyOpen(tokens, pairs, endTok, lang, lineAt, kind)
|
|
2354
|
+
last = scan.lastTok
|
|
2355
|
+
if (scan.bodyOpen !== -1) {
|
|
2356
|
+
bodyOpen = scan.bodyOpen
|
|
2357
|
+
if (kind === 'variable' && scan.sawBinding) binding = 'function'
|
|
2358
|
+
} else if (scan.uncertainTok !== -1) {
|
|
2359
|
+
spanComplete = false
|
|
2360
|
+
spanNote = `line ${lineAt(tokens[scan.uncertainTok].start)}: the header scan stopped at ${JSON.stringify(tokens[scan.uncertainTok].value)} without a block opener, and that token can neither continue the header nor start a declaration, so the span cannot be delimited`
|
|
2361
|
+
}
|
|
2362
|
+
} else {
|
|
2363
|
+
// 语言没有块括号:块体终结规则全在配置表里(`blockEnd`),扫描器里不写语言分支。
|
|
2364
|
+
const spec = lang.blockEnd
|
|
2365
|
+
if (spec === undefined) {
|
|
2366
|
+
spanComplete = false
|
|
2367
|
+
spanNote = `line ${lineAt(tokens[startTok].start)}: the language config declares no block terminator (neither bodyOpen nor blockEnd), so only the declaration line is known`
|
|
2368
|
+
} else if (spec.style === 'indent') {
|
|
2369
|
+
const baseLine = lineAt(tokens[startTok].start)
|
|
2370
|
+
const baseIndent = leadingSpace(lines[baseLine - 1] ?? '')
|
|
2371
|
+
// 头部扫描必须从**名字**那一 token 起(`startTok`),不能从规则末端 `endTok` 起。
|
|
2372
|
+
// 头部收尾符(`:` / `=`)是**头部的一部分**:从名字之后开始扫才找得到它。
|
|
2373
|
+
// 从 `endTok` 起会把收尾符自己跳过去(实测:`main = print 1` 里 `=` 就在 endTok 上,
|
|
2374
|
+
// 于是判据转去扫下一行、返回 -1,Haskell/OCaml/Nim 的符号全部 spanComplete=false)。
|
|
2375
|
+
const hit = indentBlockEnd(tokens, lines, startTok, baseLine, baseIndent, lineAt, spec.headerEnds, pairs)
|
|
2376
|
+
if (hit.endTok === -1) {
|
|
2377
|
+
spanComplete = false
|
|
2378
|
+
spanNote = hit.note
|
|
2379
|
+
} else last = hit.endTok
|
|
2380
|
+
} else if (spec.style === 'terminator') {
|
|
2381
|
+
const hit = terminatorBlockEnd(tokens, pairs, endTok, spec, lineAt, termScan(spec.terminators))
|
|
2382
|
+
if (hit.endTok === -1) {
|
|
2383
|
+
spanComplete = false
|
|
2384
|
+
spanNote = `line ${lineAt(tokens[startTok].start)}: no statement terminator closes this declaration, and no later line starts a new one, so the span cannot be delimited`
|
|
2385
|
+
} else last = hit.endTok
|
|
2386
|
+
} else if (spec.style === 'closer') {
|
|
2387
|
+
const hit = closerBlockEnd(tokens, startTok, endTok, spec, lineAt, lines)
|
|
2388
|
+
if (hit.endTok === -1) {
|
|
2389
|
+
spanComplete = false
|
|
2390
|
+
spanNote = hit.note
|
|
2391
|
+
} else last = hit.endTok
|
|
2392
|
+
} else {
|
|
2393
|
+
// 配置写错的风格名不能静默退化成签名行
|
|
2394
|
+
spanComplete = false
|
|
2395
|
+
spanNote = `line ${lineAt(tokens[startTok].start)}: unknown blockEnd style ${JSON.stringify(spec.style)}`
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
return {
|
|
2400
|
+
kind: binding,
|
|
2401
|
+
startLine: lineAt(tokens[startTok].start),
|
|
2402
|
+
endLine: lineAt(tokens[last].end),
|
|
2403
|
+
startTok,
|
|
2404
|
+
endTok: last,
|
|
2405
|
+
bodyOpen,
|
|
2406
|
+
spanComplete,
|
|
2407
|
+
spanNote,
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
// ── 结构索引的记忆化 ───────────────────────────────────────────────────────
|
|
2412
|
+
/**
|
|
2413
|
+
* `indexStructure` 是热路径:`aura_read` 的每个结构化模式、`aura_edit` 每次结构化编辑都会建一次
|
|
2414
|
+
* 完整索引(token 流 + 括号配平 + 符号表)。实测本仓最大的引擎文件(本文件,施工前 97 KB /
|
|
2415
|
+
* 2573 行)建一次 **15.8 ms**,而同一份内容在一轮里常被连问几次(read 的 map → digest → target
|
|
2416
|
+
* 各一次,edit 的 plan 与 verify 各一次)——同一份工作被反复做。这一层把它记住。
|
|
2417
|
+
*
|
|
2418
|
+
* 三条设计判据:
|
|
2419
|
+
*
|
|
2420
|
+
* 1. **键是内容本身,不是路径。** Map 的键就是源码字符串——JS 的字符串相等是逐字符的,
|
|
2421
|
+
* 因此命中即“内容一字不差”,不存在哈希碰撞这种会让“改了内容却拿到旧索引”成真的路径。
|
|
2422
|
+
* 同路径内容变了 ⇒ 键不同 ⇒ 必然重算;不同路径内容相同 ⇒ 键相同 ⇒ 命中
|
|
2423
|
+
* (这正是要的:索引只取决于内容)。实测 97 KB 字符串的 `Map.get` 约 28 µs,
|
|
2424
|
+
* 是重算代价的约 1/560。
|
|
2425
|
+
* 2. **语言单独比对,且按对象身份。** `langOf()` 返回的是 `LANGS` 表里的单例
|
|
2426
|
+
* (与 `DECLARATION_STARTS` 同一套做法):身份不同即未命中,结构相同的两个语言对象
|
|
2427
|
+
* 只会各自重算,绝不会互相返回对方的索引——错命中比未命中危险得多。
|
|
2428
|
+
* 3. **有界。** 条目数与估算字节双上限,超出按 LRU 淘汰(Map 的插入序即访问序,
|
|
2429
|
+
* 命中时 delete+set 就把它挪到队尾)。字节那一条是**按可用内存算出来的额度**(不是一个拍
|
|
2430
|
+
* 出来的常数),三个输入、四种落点、为何本机安全,都写在下方额度块里。
|
|
2431
|
+
*
|
|
2432
|
+
* 对调用方**完全透明**:返回值逐字段与不缓存时相同(`index-cache-test.mjs` 段C 做全量深比较);
|
|
2433
|
+
* 命中时返回的是**浅拷贝**,因此“每次调用都拿到自己的顶层对象”这条性质不变(调用方往索引上
|
|
2434
|
+
* 挂字段不会漏给下一次调用)。载荷(tokens / symbols / pairs / lineAt)是共享的,调用方须当只读:
|
|
2435
|
+
* 本仓全部消费者都是只读的(`listSymbols` / `rankSymbols` 都产出新数组,`resolveTarget` 展开复制)。
|
|
2436
|
+
*/
|
|
2437
|
+
|
|
2438
|
+
/**
|
|
2439
|
+
* ── 额度(内存预算)怎么定:三个可测量的量,不是一个拍出来的常数 ─────────────
|
|
2440
|
+
*
|
|
2441
|
+
* ① **单份索引的实测字节**(定下限)。目标档是「100000 行 / 2.4 MB 的源码」。本机实测
|
|
2442
|
+
* (node v24.15.0,Windows,用 `--expose-gc` 量 `heapUsed` 常驻):这样一份索引**实测常驻
|
|
2443
|
+
* 40.91 MiB**,{@link structureBytes} 估 51.75 MiB(估算是上界,比值见该函数)。额度必须
|
|
2444
|
+
* ≥ 单份的两倍——否则“目标档必然未命中 ⇒ 每次调用都重建”这个 bug 就会回来:旧常数 32 MiB
|
|
2445
|
+
* 正是如此,90000 行(est 36.8 MiB)起就全部未命中、冷热同价(本机复现:`readWith(mode=lines)`
|
|
2446
|
+
* 90000 行冷 5.5 s / 热 5.8 s;95000 行冷 7.1 s / 热 8.2 s)。
|
|
2447
|
+
* ② **进程可见的可用内存** `process.availableMemory()`(定基准)。额度 = 它的 1/16。
|
|
2448
|
+
* ③ **绝对下限 96 MiB / 上限 256 MiB**(定边界)。下限 ≈ 目标档单份实测的两倍多;上限则是因为
|
|
2449
|
+
* 缓存不该成为进程的主内存项(宿主进程整只 RSS 也才几百 MiB)。
|
|
2450
|
+
*
|
|
2451
|
+
* 合起来:`额度 = clamp(可用内存/16, min(96 MiB, 可用内存/4), 256 MiB)`。四种落点都有意义:
|
|
2452
|
+
* · 本机(实测可用内存 2.7~3.6 GiB 波动)⇒ **182~222 MiB**(本次交付跑出 182.55 MiB),
|
|
2453
|
+
* 占**可用**内存 6.25%、占总内存 1.4%,目标档稳进;
|
|
2454
|
+
* · 可用内存中等(< 1.5 GiB)⇒ 落到 96 MiB 下限,仍是目标档单份实测的两倍多;
|
|
2455
|
+
* · 可用内存告急(< 384 MiB)⇒ 下限随可用内存缩到 1/4,宁可少缓存也不跟别人抢内存;
|
|
2456
|
+
* · 取不到可用内存(非 node 宿主 / 老版本)⇒ 退回 96 MiB 下限,不赌。
|
|
2457
|
+
*
|
|
2458
|
+
* 为什么本机这一档(180 MiB 上下)是安全的(三条都能复核,不是一句话):
|
|
2459
|
+
* · 它是**估算**字节,而估算在这台机器上实测**盖得住**真实常驻(est/real 实测 1.2~2.0,
|
|
2460
|
+
* 即真实占用比额度小两成以上;交付读数:上限 169.51 MiB 时 gc 后实测常驻 144.17 MiB;
|
|
2461
|
+
* 段B 直接在目标档上断言 est ≥ 实测);
|
|
2462
|
+
* · 占可用内存 6.25%、占总内存 1.4%,与宿主进程现有 RSS 同量级——不是“再开一个进程”;
|
|
2463
|
+
* · 额度要被“真的索引过那么多内容”才兑现,且条目上限 16 同时生效:不是一上来就吃掉两百兆。
|
|
2464
|
+
*
|
|
2465
|
+
* 额度**定一次、进程内不变**:可用内存会随机器负载漂移,而缓存判定必须确定(同一份内容在同一
|
|
2466
|
+
* 进程里只能有一个结论)。要改这个数用环境变量 `AURA_INDEX_CACHE_MB` / `AURA_INDEX_CACHE_BYTES`
|
|
2467
|
+
* (读一次,同样被 [8 MiB, 256 MiB] 夹住)——**可配,但不许把机器打爆**。
|
|
2468
|
+
*
|
|
2469
|
+
* ── 超限怎么淘汰:LRU + 按字节预算(两个上限同时生效,谁先到谁触发)───────────
|
|
2470
|
+
* · **字节预算**(上面那个额度)管大文件:16 条之内也可能超额度,那时按访问序淘汰最旧的;
|
|
2471
|
+
* · **条目 16** 管小文件:一轮对话里真正碰到的文件是个位数(被读的文件 + 它的编辑目标 +
|
|
2472
|
+
* 少数邻居),16 是两倍余量;小文件的字节远够不到额度,靠条目数兜底。
|
|
2473
|
+
* 单份索引本身就超额度 ⇒ 不缓存(它每次重算,也不许挤掉整个缓存):这是唯一“未命中不入缓存”的路径。
|
|
2474
|
+
* 没有这两个上限,缓存就是用内存换速度的无界泄漏。
|
|
2475
|
+
*/
|
|
2476
|
+
const INDEX_CACHE_MAX_ENTRIES = 16
|
|
2477
|
+
/** 下限:≈ 目标档(100000 行 / 实测 40.91 MiB)单份索引的两倍多。 */
|
|
2478
|
+
const INDEX_CACHE_MIN_BYTES = 96 * 1024 * 1024
|
|
2479
|
+
/** 上限:缓存不许成为进程的主内存项(宿主整只 RSS 也才几百 MiB)。 */
|
|
2480
|
+
const INDEX_CACHE_MAX_BYTES = 256 * 1024 * 1024
|
|
2481
|
+
/** 环境变量可调区间(用户可以往下调,不许往上越过上限)。 */
|
|
2482
|
+
const INDEX_CACHE_ENV_MIN_BYTES = 8 * 1024 * 1024
|
|
2483
|
+
/** 基准份额:可用内存的 1/16。 */
|
|
2484
|
+
const INDEX_CACHE_SHARE = 16
|
|
2485
|
+
/** 可用内存告急时,下限压到可用内存的 1/4(宁可少缓存,也不跟别人抢内存)。 */
|
|
2486
|
+
const INDEX_CACHE_LOW_MEMORY_SHARE = 4
|
|
2487
|
+
|
|
2488
|
+
/**
|
|
2489
|
+
* 进程可见的可用内存;取不到(非 node 宿主 / 老版本 / 读数非法)返回 null。
|
|
2490
|
+
*
|
|
2491
|
+
* 引擎**不 import 任何 node 内建模块**(“可搬走”是它对外的承诺),因此这里只用全局 `process`:
|
|
2492
|
+
* `process.availableMemory()` 自 Node 22 起存在。
|
|
2493
|
+
*/
|
|
2494
|
+
function availableMemoryBytes() {
|
|
2495
|
+
if (typeof process === 'undefined' || typeof process.availableMemory !== 'function') return null
|
|
2496
|
+
try {
|
|
2497
|
+
const bytes = process.availableMemory()
|
|
2498
|
+
return Number.isFinite(bytes) && bytes > 0 ? bytes : null
|
|
2499
|
+
} catch {
|
|
2500
|
+
return null
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
/** 环境变量覆写:`AURA_INDEX_CACHE_MB`(优先,便于手写)> `AURA_INDEX_CACHE_BYTES`;被 [8 MiB, 256 MiB] 夹住。 */
|
|
2505
|
+
function envBudgetBytes() {
|
|
2506
|
+
if (typeof process === 'undefined' || process.env === undefined || process.env === null) return null
|
|
2507
|
+
const pick = (name, scale) => {
|
|
2508
|
+
const raw = process.env[name]
|
|
2509
|
+
if (typeof raw !== 'string' || raw.trim().length === 0) return null
|
|
2510
|
+
const value = Number(raw.trim())
|
|
2511
|
+
return Number.isFinite(value) && value > 0 ? value * scale : null
|
|
2512
|
+
}
|
|
2513
|
+
const bytes = pick('AURA_INDEX_CACHE_MB', 1024 * 1024) ?? pick('AURA_INDEX_CACHE_BYTES', 1)
|
|
2514
|
+
if (bytes === null) return null
|
|
2515
|
+
return Math.min(INDEX_CACHE_MAX_BYTES, Math.max(INDEX_CACHE_ENV_MIN_BYTES, bytes))
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
/**
|
|
2519
|
+
* `额度 = clamp(可用内存/16, min(96 MiB, 可用内存/4), 256 MiB)`——取值理由见上方长注释。
|
|
2520
|
+
* `source` 记的是**这台机器上这个数是怎么来的**,给观测与测试用({@link indexCacheStats})。
|
|
2521
|
+
*/
|
|
2522
|
+
function indexCacheBudget() {
|
|
2523
|
+
const fromEnv = envBudgetBytes()
|
|
2524
|
+
if (fromEnv !== null) return { bytes: fromEnv, source: 'env(AURA_INDEX_CACHE_*)' }
|
|
2525
|
+
const available = availableMemoryBytes()
|
|
2526
|
+
if (available === null) return { bytes: INDEX_CACHE_MIN_BYTES, source: 'floor(no-available-memory)' }
|
|
2527
|
+
const floor = Math.min(INDEX_CACHE_MIN_BYTES, available / INDEX_CACHE_LOW_MEMORY_SHARE)
|
|
2528
|
+
const share = Math.min(INDEX_CACHE_MAX_BYTES, available / INDEX_CACHE_SHARE)
|
|
2529
|
+
if (share >= floor) return { bytes: share, source: 'available-memory/16' }
|
|
2530
|
+
return { bytes: floor, source: available / INDEX_CACHE_LOW_MEMORY_SHARE < INDEX_CACHE_MIN_BYTES ? 'available-memory/4(low)' : 'floor' }
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
/** 本进程的额度(**定一次**)。 */
|
|
2534
|
+
const INDEX_CACHE_BUDGET = indexCacheBudget()
|
|
2535
|
+
|
|
2536
|
+
/** `Map<源码字符串, {lang, index, bytes}>`;插入序即 LRU 序(队首最旧)。 */
|
|
2537
|
+
const INDEX_CACHE = new Map()
|
|
2538
|
+
let INDEX_CACHE_BYTES = 0
|
|
2539
|
+
|
|
2540
|
+
/**
|
|
2541
|
+
* 一条索引的常驻字节**上界估算**:src 本身 + token / 符号对象 + 配平表。
|
|
2542
|
+
*
|
|
2543
|
+
* 系数是**量出来的**,不是拍的:在 7 种形状上各量一次真实常驻(`--expose-gc` 前后 `heapUsed` 差,
|
|
2544
|
+
* 每份都留活口以免被 GC 拉负),得到每 token ≈ 93 B、每符号 ≈ 245 B、每配对 ≈ 0~30 B、
|
|
2545
|
+
* 每字符 ≤ 2 B(V8 对源码字符串用 Latin-1 或 UTF-16,两种表示都不超过 2 B/字符)。
|
|
2546
|
+
* 这里取 96 / 288 / 64 / 2(即 ~20% 余量)后,实测 est/real 落在 **1.19~2.02**:
|
|
2547
|
+
*
|
|
2548
|
+
* 形状 src 字符 token symbol pair 实测 MiB est MiB est/real
|
|
2549
|
+
* 块状 JS 20000 行 478665 79992 5713 8575 8.40 10.33 1.23
|
|
2550
|
+
* 块状 JS 50000 行 1206898 200000 14285 21433 20.15 25.84 1.28
|
|
2551
|
+
* **块状 JS 100000 行(目标档)** 2446271 400004 28571 42862 40.91 51.75 1.27
|
|
2552
|
+
* 长字符串 / 少 token 5000 行 983890 20000 5000 0 2.52 5.08 2.02
|
|
2553
|
+
* 每行 20 token 5000 行 222230 85000 0 10000 7.38 8.82 1.19
|
|
2554
|
+
* 深层嵌套 5000 行 182780 140000 0 65000 11.89 17.13 1.44
|
|
2555
|
+
* 本仓引擎文件(97 KB) 101412 15814 248 1568 1.39 1.81 1.30
|
|
2556
|
+
*
|
|
2557
|
+
* 它**必须盖得住**实测:额度就是按这个数记账的,估算偏低 ⇒ 缓存的真实占用越过额度。
|
|
2558
|
+
* 旧公式(`src*4 + token*64 + symbol*192 + pair*48`)在 token 密集档上正是如此(每行 20 token 那档
|
|
2559
|
+
* 只有 0.88,即“额度说没超、内存已经超了”)。偏大只会更早淘汰,不会漏淘汰;
|
|
2560
|
+
* 而“盖得住”这件事由 `index-cache-test.mjs` 段B 在**目标档**上直接断言(est ≥ 实测),
|
|
2561
|
+
* 不靠这句注释。
|
|
2562
|
+
*/
|
|
2563
|
+
function structureBytes(index) {
|
|
2564
|
+
return index.src.length * 2 + index.tokens.length * 96 + index.symbols.length * 288 + index.pairs.size * 64
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
/** 淘汰到预算以内;队首(最近最少用)先走。两个上限:字节预算(大文件)+ 条目数(小文件)。 */
|
|
2568
|
+
function evictStructureCache() {
|
|
2569
|
+
while (INDEX_CACHE.size > INDEX_CACHE_MAX_ENTRIES || INDEX_CACHE_BYTES > INDEX_CACHE_BUDGET.bytes) {
|
|
2570
|
+
const oldest = INDEX_CACHE.keys().next()
|
|
2571
|
+
if (oldest.done === true) return
|
|
2572
|
+
const entry = INDEX_CACHE.get(oldest.value)
|
|
2573
|
+
INDEX_CACHE.delete(oldest.value)
|
|
2574
|
+
INDEX_CACHE_BYTES -= entry.bytes
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
/**
|
|
2579
|
+
* 真正的建索引:token 流 + 括号配平 + 符号表。**不缓存**——`indexStructure` 是它的记忆化外壳。
|
|
2580
|
+
* 拆成两个名字是为了让“缓存”这件事只出现在一个地方:这里一行都不谈命中与淘汰。
|
|
2581
|
+
*/
|
|
2582
|
+
function buildStructure(src, lang) {
|
|
2583
|
+
const tokens = tokenize(src, lang)
|
|
2584
|
+
const pairs = matchBrackets(tokens, lang)
|
|
2585
|
+
const lineAt = lineIndexFor(src)
|
|
2586
|
+
// 缩进型语言的块体终结要看行文本(前导空白),build 一次供全部符号共用
|
|
2587
|
+
const lines = src.split(/\r?\n/)
|
|
2588
|
+
// 终结符型语言(SQL / Erlang / Vim)的块体终结走跳表:**每次建索引只建一次**,见 terminatorBlockEnd
|
|
2589
|
+
const termScan = lazyTerminatorScan(tokens, pairs, lineAt)
|
|
2590
|
+
const symbols = []
|
|
2591
|
+
|
|
2592
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
2593
|
+
for (const rule of lang.rules) {
|
|
2594
|
+
const hit = matchRule(tokens, i, rule, lang, pairs, lineAt)
|
|
2595
|
+
if (hit === null) continue
|
|
2596
|
+
// 声明起点前是修饰符 → 这一处不是起点(真正的起点在修饰符之前那一次命中)。
|
|
2597
|
+
//
|
|
2598
|
+
// 必须查 **matchStart**(规则起点),不能查 `startTok`(名字位置)。
|
|
2599
|
+
// 查错的后果:`export function alpha` 会在 i=0(规则起点 `export`)与 i=1(规则起点
|
|
2600
|
+
// `function`)各命中一次;i=1 那次若去看 `tokens[1-1]` 即 `function`,它不是修饰符,
|
|
2601
|
+
// 闸门放行,于是同一处被推两次、坐标完全相同,`target=alpha` 直接报歧义失效。
|
|
2602
|
+
if (precededByModifier(tokens, hit.matchStart, lang)) continue
|
|
2603
|
+
// `$label` 的捕获值可能带引号(`"aws_instance"`):符号名要的是**名字本身**。
|
|
2604
|
+
// 剥引号只在这里做一次——`matchRule` 那边必须保留原样才有办法反查名字 token(见该函数)。
|
|
2605
|
+
const name = typeof hit.cap.$name === 'string' ? hit.cap.$name.replace(/^['"]|['"]$/g, '') : hit.cap.$name
|
|
2606
|
+
// 符号名必须是**标识符形状**。这是所有语言共用的一条硬闸门,不是“新语言专用补丁”:
|
|
2607
|
+
// 没有它,那些**没有前导字面量**的规则(Haskell 的 `name ::`、Nim 的 `name(...)=`)
|
|
2608
|
+
// 会捕到参数列表里的 `a:` 这种半截 token 并报出一个不存在的符号。
|
|
2609
|
+
// 它比“只捕标识符”的 `$name` 严——`$name` 不校验**字符串**内容,而 `$label` 会带引号进来。
|
|
2610
|
+
if (typeof name !== 'string' || !/^[A-Za-z_$\u0080-\uffff][A-Za-z0-9_$\u0080-\uffff-]*$/.test(name)) continue
|
|
2611
|
+
const span = declarationSpan(tokens, pairs, hit.startTok, hit.end - 1, lang, lineAt, rule.kind, lines, termScan)
|
|
2612
|
+
symbols.push({ name, ...span })
|
|
2613
|
+
break
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
// 归属(enclosing):每个符号最内层的包含者。字段语义、旧实现的实测代价、新解法的等价性
|
|
2618
|
+
// 论证、以及「区间重叠但不包含时给出什么」,全部写在 {@link attributeEnclosing} 上方那一段里。
|
|
2619
|
+
attributeEnclosing(symbols)
|
|
2620
|
+
|
|
2621
|
+
return { symbols, tokens, pairs, lineAt, src }
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
/**
|
|
2625
|
+
* 归属(`enclosing`):每个符号**最内层**的包含者。**一趟扫掠 + 后缀最小值,O(n log n)**。
|
|
2626
|
+
*
|
|
2627
|
+
* ── 这个字段的语义(对外契约,不许改)────────────────────────────────────────
|
|
2628
|
+
*
|
|
2629
|
+
* 对每个符号 `s`,答案是符号表里**第一个**满足下面三条的 `c`(次序 = 宽度升序、宽度并列时按
|
|
2630
|
+
* `symbols` 原序):
|
|
2631
|
+
*
|
|
2632
|
+
* c !== s 且 c.startTok <= s.startTok 且 c.endTok >= s.endTok 且 宽度(c) > 宽度(s)
|
|
2633
|
+
*
|
|
2634
|
+
* 换个说法:**容器里最窄的那个;宽度并列时取在 `symbols` 里靠前的那个**;没有容器 ⇒ `null`。
|
|
2635
|
+
* 这不是「能算就行」的装饰字段:没有它,`todos` 在文件里出现两次就无法区分——不是匹配算法
|
|
2636
|
+
* 不够聪明,是模型缺字段(那一课写在 `buildStructure` 里)。
|
|
2637
|
+
*
|
|
2638
|
+
* ── 旧实现为什么必须换掉(实测,不是推断)─────────────────────────────────
|
|
2639
|
+
*
|
|
2640
|
+
* 旧实现把符号表按宽度排一遍,然后**每个符号都扫一遍全表**。平坦文件里没有任何符号包含别的
|
|
2641
|
+
* 符号 ⇒ 每个符号都扫满全表 ⇒ 完整的 O(n²)。本机复跑(内容 = `export const vN = N;` × N 行,
|
|
2642
|
+
* 每次都拿一个全新模块实例当冷路径):
|
|
2643
|
+
*
|
|
2644
|
+
* N(符号=行) 旧实现冷索引 µs/行 | 对照:同样行数、零符号的文件
|
|
2645
|
+
* 5000 110.8 ms 22.2 | 20000 行 106.7 ms(5.3 µs/行)
|
|
2646
|
+
* 10000 382.2 ms 38.2 | 40000 行 133.7 ms(3.3 µs/行)
|
|
2647
|
+
* 20000 1311.3 ms 65.6 |
|
|
2648
|
+
* 40000 7081.4 ms 177.0 |
|
|
2649
|
+
* 100000 67579.0 ms 675.8 |
|
|
2650
|
+
*
|
|
2651
|
+
* 每行成本随规模上升 = 超线性,而扫描器本身是平的(右列)。`--cpu-prof` 的行级 tick 把账算死:
|
|
2652
|
+
* 40000 符号档 `buildStructure` 自身 3763 个采样点里,**2530 / 2531 / 2532 三行占 3732 个
|
|
2653
|
+
* (99.2%)**,而 tokenize / matchRule / matchBrackets / scanBodyOpen 加起来不到 1%
|
|
2654
|
+
* (`declarationSpan` 只有 4 个点)。也就是说「一次读卡几十秒」全部来自这一段:不是壳、不是 IO、
|
|
2655
|
+
* 不是账本。
|
|
2656
|
+
*
|
|
2657
|
+
* ── 新解法:按 startTok 扫掠 + 按 endTok 的后缀最小值(Fenwick 树)─────────────
|
|
2658
|
+
*
|
|
2659
|
+
* 容器判据是一个**二维支配**查询(`c.startTok <= s.startTok` 且 `c.endTok >= s.endTok`):
|
|
2660
|
+
* · 按 `startTok` 升序扫掠 ⇒ 已经插进树里的 `c` 天然满足前半条;
|
|
2661
|
+
* · 后半条由一棵**按 endTok 建的后缀最小值树**回答(endTok 越大位置越靠前,于是「后缀」就是
|
|
2662
|
+
* Fenwick 的前缀查询);
|
|
2663
|
+
* · 树里存的键是 `(宽度, 原下标)` 的**字典序**——这正是旧实现「按宽度升序、并列按原序」那条
|
|
2664
|
+
* 次序,所以「树里的最小值」就是旧实现「第一个命中的容器」。
|
|
2665
|
+
*
|
|
2666
|
+
* 逐条等价性论证(不是「看起来一样」;段C 在真实语料上再逐符号对一遍):
|
|
2667
|
+
*
|
|
2668
|
+
* 1. **同区间的符号答案必然相同。** 判据只看 `(startTok, endTok)`,同区间的符号宽度也相同;
|
|
2669
|
+
* 它们互为候选时都被「宽度严格更大」挡掉(包含 + 宽度相等 ⇒ `(startTok, endTok)` 全等)。
|
|
2670
|
+
* 于是同区间并成一组、算一次、写给全组 ⇒ 逐条等于旧实现。
|
|
2671
|
+
* 2. **组内按 endTok 降序、每组「先问后插」。** 问 `s` 时树里恰好是
|
|
2672
|
+
* `{c : c.startTok < s.startTok} ∪ {c : c.startTok == s.startTok 且 c.endTok > s.endTok}`:
|
|
2673
|
+
* 前者由扫掠序保证,后者由「同 start 组按 end 降序 + 先问后插」保证。`c.endTok < s.endTok`
|
|
2674
|
+
* 的本来也包含不了 `s`;`(startTok, endTok)` 全等的那一组就是 `s` 自己(还没插进去)。
|
|
2675
|
+
* ⇒ 树里**恰好**是全部容器,不多不少。
|
|
2676
|
+
* 3. **「宽度严格更大」不必再查。** `c` 包含 `s` 且不是 `s` 那一组 ⇒ `宽度(c) >= 宽度(s)`,
|
|
2677
|
+
* 取等只在 `(startTok, endTok)` 全等 ⇒ 自动严格更大。所以查到的每一个都合法。
|
|
2678
|
+
*
|
|
2679
|
+
* ── 区间**不严格嵌套**(重叠但不包含)时给出什么 ─────────────────────────────
|
|
2680
|
+
*
|
|
2681
|
+
* 这一条不能靠「代码看起来对」过关,逐条说清:
|
|
2682
|
+
* · 上面的论证**没有一处用到嵌套**:新解法回答的就是「满足那两条不等式的容器里键最小的
|
|
2683
|
+
* 那个」,与旧实现的判据逐字同源。因此**交叉区间两边都给 `null`**——例如 A=[1,100]、
|
|
2684
|
+
* B=[50,150]:A 的容器要 `c.startTok <= 1`,而 B 的 startTok 是 50;B 的容器要
|
|
2685
|
+
* `c.endTok >= 150`,而 A 的 endTok 是 100。旧实现给的就是这个答案,新解法给同一个。
|
|
2686
|
+
* · 会给出**不同**答案的是「按 start 排序后用栈顶当包含者」那种经典写法:对 A=[0,100]、
|
|
2687
|
+
* B=[5,20] 这类「后开的区间反而更长」的形状,它会把 B 报成 A 的包含者(旧实现判 `null`,
|
|
2688
|
+
* 因为 `B.endTok = 20 < A.endTok = 100`)。**本实现没有采用那个写法**,正是因为它在这类
|
|
2689
|
+
* 输入上不等价。段C 因此除了真实语料,还单独构造交叉 / 同 start / 同 end / 等宽的形状逐条比。
|
|
2690
|
+
*
|
|
2691
|
+
* 复杂度:排序 O(n log n) + 树 O(n log m)(n = 符号数,m = 不同 endTok 数)。
|
|
2692
|
+
*
|
|
2693
|
+
* @param {Array<object>} symbols - `buildStructure` 的符号表;**原地**写 `enclosing`
|
|
2694
|
+
*/
|
|
2695
|
+
function attributeEnclosing(symbols) {
|
|
2696
|
+
const n = symbols.length
|
|
2697
|
+
for (let i = 0; i < n; i += 1) symbols[i].enclosing = null
|
|
2698
|
+
if (n < 2) return // 一个符号不可能有包含者;零个符号连扫掠都不必进
|
|
2699
|
+
|
|
2700
|
+
// 扫掠序:startTok 升序;同 start 时 endTok **降序**(见论证 2);末位按原下标收尾(全序 ⇒ 确定性)。
|
|
2701
|
+
const ord = new Array(n)
|
|
2702
|
+
for (let i = 0; i < n; i += 1) ord[i] = i
|
|
2703
|
+
ord.sort((a, b) => {
|
|
2704
|
+
const byStart = symbols[a].startTok - symbols[b].startTok
|
|
2705
|
+
if (byStart !== 0) return byStart
|
|
2706
|
+
const byEnd = symbols[b].endTok - symbols[a].endTok
|
|
2707
|
+
return byEnd !== 0 ? byEnd : a - b
|
|
2708
|
+
})
|
|
2709
|
+
|
|
2710
|
+
// endTok 的压缩排名:`posOf(end) = m - rank` ⇒ 「endTok >= s.endTok」就是 Fenwick 的**前缀**查询。
|
|
2711
|
+
const ends = new Array(n)
|
|
2712
|
+
for (let i = 0; i < n; i += 1) ends[i] = symbols[i].endTok
|
|
2713
|
+
ends.sort((a, b) => a - b)
|
|
2714
|
+
let m = 0
|
|
2715
|
+
for (let i = 0; i < n; i += 1) {
|
|
2716
|
+
if (i > 0 && ends[i] === ends[i - 1]) continue
|
|
2717
|
+
ends[m] = ends[i]
|
|
2718
|
+
m += 1
|
|
2719
|
+
}
|
|
2720
|
+
const posOf = new Map()
|
|
2721
|
+
for (let r = 0; r < m; r += 1) posOf.set(ends[r], m - r)
|
|
2722
|
+
|
|
2723
|
+
// 最小值树的键是 (宽度, 原下标) 的字典序;只做「插入时取更小」,正是 Fenwick 前缀最小值的前提。
|
|
2724
|
+
const treeWidth = new Float64Array(m + 1).fill(Infinity)
|
|
2725
|
+
const treeIndex = new Int32Array(m + 1).fill(-1)
|
|
2726
|
+
|
|
2727
|
+
let g = 0
|
|
2728
|
+
while (g < n) {
|
|
2729
|
+
const head = ord[g]
|
|
2730
|
+
const startTok = symbols[head].startTok
|
|
2731
|
+
const endTok = symbols[head].endTok
|
|
2732
|
+
let stop = g + 1
|
|
2733
|
+
while (stop < n) {
|
|
2734
|
+
const j = ord[stop]
|
|
2735
|
+
if (symbols[j].startTok !== startTok || symbols[j].endTok !== endTok) break
|
|
2736
|
+
stop += 1
|
|
2737
|
+
}
|
|
2738
|
+
const pos = posOf.get(endTok)
|
|
2739
|
+
|
|
2740
|
+
// 先问:此刻树里恰好是全部容器(论证 2)。
|
|
2741
|
+
let bestWidth = Infinity
|
|
2742
|
+
let bestIndex = -1
|
|
2743
|
+
for (let i = pos; i > 0; i -= i & -i) {
|
|
2744
|
+
const w = treeWidth[i]
|
|
2745
|
+
if (w < bestWidth || (w === bestWidth && treeIndex[i] < bestIndex)) {
|
|
2746
|
+
bestWidth = w
|
|
2747
|
+
bestIndex = treeIndex[i]
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
const enclosing = bestIndex === -1 ? null : symbols[bestIndex].name
|
|
2751
|
+
for (let k = g; k < stop; k += 1) symbols[ord[k]].enclosing = enclosing
|
|
2752
|
+
|
|
2753
|
+
// 后插:一组只占一条记录(同区间的后来者答案完全相同、宽度也相同 ⇒ 谁也当不了谁的容器)。
|
|
2754
|
+
const width = endTok - startTok
|
|
2755
|
+
for (let i = pos; i <= m; i += i & -i) {
|
|
2756
|
+
const w = treeWidth[i]
|
|
2757
|
+
if (width < w || (width === w && head < treeIndex[i])) {
|
|
2758
|
+
treeWidth[i] = width
|
|
2759
|
+
treeIndex[i] = head
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
g = stop
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
/**
|
|
2767
|
+
* 建结构索引:符号表 + 作用域配对表。
|
|
2768
|
+
*
|
|
2769
|
+
* **同一份内容重复调用不再重复解析**:命中判据、上限与透明性见本函数上方的记忆化说明。
|
|
2770
|
+
* 对调用方而言签名、返回形状与语义都没有变——缓存只在“这份内容刚算过、语言也一致”时生效。
|
|
2771
|
+
*
|
|
2772
|
+
* @param {string} src - 源码全文
|
|
2773
|
+
* @param {object} lang - 语言配置
|
|
2774
|
+
* @returns {{symbols: Array, tokens: Array, pairs: Map, lineAt: Function}}
|
|
2775
|
+
*/
|
|
2776
|
+
export function indexStructure(src, lang) {
|
|
2777
|
+
/**
|
|
2778
|
+
* 非字符串不进缓存。
|
|
2779
|
+
*
|
|
2780
|
+
* Map 对非字符串键按**对象身份**比对,而调用方完全可能复用同一个 Buffer 反复装填内容
|
|
2781
|
+
* (读文件循环里的常见写法)——那正好会让“改了内容却命中旧索引”真的发生。
|
|
2782
|
+
* 不缓存就与改动前逐字一致,这正是这里要的:正确性优先于命中率。
|
|
2783
|
+
*/
|
|
2784
|
+
if (typeof src !== 'string') return buildStructure(src, lang)
|
|
2785
|
+
|
|
2786
|
+
const hit = INDEX_CACHE.get(src)
|
|
2787
|
+
if (hit !== undefined && hit.lang === lang) {
|
|
2788
|
+
// LRU:命中即刷新访问序(Map 保持插入序,先删再插 = 挪到队尾)。
|
|
2789
|
+
INDEX_CACHE.delete(src)
|
|
2790
|
+
INDEX_CACHE.set(src, hit)
|
|
2791
|
+
return { ...hit.index }
|
|
2792
|
+
}
|
|
2793
|
+
const index = buildStructure(src, lang)
|
|
2794
|
+
const bytes = structureBytes(index)
|
|
2795
|
+
// 单条就超额度:宁可它每次重算,也不让一条挤掉整个缓存(额度取值见上方长注释)。
|
|
2796
|
+
if (bytes > INDEX_CACHE_BUDGET.bytes) return index
|
|
2797
|
+
if (hit !== undefined) {
|
|
2798
|
+
// 同一份内容配了另一个语言对象:旧的那份对本调用方没用,替换而不是并存。
|
|
2799
|
+
INDEX_CACHE.delete(src)
|
|
2800
|
+
INDEX_CACHE_BYTES -= hit.bytes
|
|
2801
|
+
}
|
|
2802
|
+
INDEX_CACHE.set(src, { lang, index, bytes })
|
|
2803
|
+
INDEX_CACHE_BYTES += bytes
|
|
2804
|
+
evictStructureCache()
|
|
2805
|
+
return { ...index }
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
/**
|
|
2809
|
+
* 缓存现状快照(**只读观测**:不改任何行为、不动缓存、不影响命中)。
|
|
2810
|
+
*
|
|
2811
|
+
* 存在的理由:额度是个**从可用内存算出来的数**,所以“上限是多少、现在用了多少”必须是能在外部
|
|
2812
|
+
* 复核的事实——测试要断言「实际字节 ≤ 上限」,运维要知道缓存吃了多少内存。没有这个口子,判据
|
|
2813
|
+
* 只能去读源码里的常数(那正是本轮要消灭的东西)。
|
|
2814
|
+
*
|
|
2815
|
+
* `bytes` 是 {@link structureBytes} 的**估算**合计(不是精确账本);`availableBytes` 取不到时为 null。
|
|
2816
|
+
* 返回的是每次新建的普通对象,不冻结:它是给人看的读数,不是内部状态本身。
|
|
2817
|
+
*
|
|
2818
|
+
* @returns {{entries: number, bytes: number, maxBytes: number, maxEntries: number, budgetSource: string, availableBytes: number|null}}
|
|
2819
|
+
*/
|
|
2820
|
+
export function indexCacheStats() {
|
|
2821
|
+
return {
|
|
2822
|
+
entries: INDEX_CACHE.size,
|
|
2823
|
+
bytes: INDEX_CACHE_BYTES,
|
|
2824
|
+
maxBytes: INDEX_CACHE_BUDGET.bytes,
|
|
2825
|
+
maxEntries: INDEX_CACHE_MAX_ENTRIES,
|
|
2826
|
+
budgetSource: INDEX_CACHE_BUDGET.source,
|
|
2827
|
+
availableBytes: availableMemoryBytes(),
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
/**
|
|
2832
|
+
* 把符号查询解析成唯一区域。
|
|
2833
|
+
*
|
|
2834
|
+
* 目标写法(可组合):
|
|
2835
|
+
* `name` 按名字
|
|
2836
|
+
* `kind:name` 限定种类(function/class/variable)
|
|
2837
|
+
* `Outer/Inner` 限定归属,消除同名嵌套歧义
|
|
2838
|
+
* `name#2` 第 2 个同名(1-based)
|
|
2839
|
+
* 三者可叠加:`function:apply/todos#1`
|
|
2840
|
+
*
|
|
2841
|
+
* @param {object} index - indexStructure 的结果
|
|
2842
|
+
* @param {string} target - 目标写法
|
|
2843
|
+
* @param {number|null} near - 可选行号提示,用于同名消歧
|
|
2844
|
+
* @returns {{startLine, endLine, name, kind}} 1-based 行区间
|
|
2845
|
+
* @throws 找不到或歧义时抛出带 code 的错误
|
|
2846
|
+
*/
|
|
2847
|
+
export function resolveTarget(index, target, near = null) {
|
|
2848
|
+
const raw = String(target ?? '').trim()
|
|
2849
|
+
if (raw.length === 0) throw Object.assign(new Error('empty target'), { code: 'STRUCT_EMPTY_TARGET' })
|
|
2850
|
+
|
|
2851
|
+
let kind = null
|
|
2852
|
+
let rest = raw
|
|
2853
|
+
const colon = raw.indexOf(':')
|
|
2854
|
+
if (colon > 0) {
|
|
2855
|
+
kind = raw.slice(0, colon)
|
|
2856
|
+
rest = raw.slice(colon + 1)
|
|
2857
|
+
}
|
|
2858
|
+
let ordinal = null
|
|
2859
|
+
const hash = rest.lastIndexOf('#')
|
|
2860
|
+
if (hash > 0) {
|
|
2861
|
+
const n = Number.parseInt(rest.slice(hash + 1), 10)
|
|
2862
|
+
if (Number.isInteger(n) && n > 0) {
|
|
2863
|
+
ordinal = n
|
|
2864
|
+
rest = rest.slice(0, hash)
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
// 归属限定:`Outer/Inner`,取最后一段为名字,前面全部是归属链
|
|
2868
|
+
const parts = rest.split('/').map((p) => p.trim()).filter((p) => p.length > 0)
|
|
2869
|
+
if (parts.length === 0) throw Object.assign(new Error('empty target'), { code: 'STRUCT_EMPTY_TARGET' })
|
|
2870
|
+
const name = parts[parts.length - 1]
|
|
2871
|
+
const scope = parts.length > 1 ? parts[parts.length - 2] : null
|
|
2872
|
+
|
|
2873
|
+
const match = (s) => s.name === name && (kind === null || s.kind === kind)
|
|
2874
|
+
let hits = index.symbols.filter(match)
|
|
2875
|
+
if (hits.length === 0) {
|
|
2876
|
+
// 退一步:唯一前缀匹配。只在前缀唯一时接受,否则仍算找不到。
|
|
2877
|
+
const prefixed = index.symbols.filter((s) => s.name.startsWith(name) && (kind === null || s.kind === kind))
|
|
2878
|
+
const distinct = [...new Set(prefixed.map((s) => s.name))]
|
|
2879
|
+
if (distinct.length === 1) hits = prefixed
|
|
2880
|
+
}
|
|
2881
|
+
if (hits.length > 0 && scope !== null) {
|
|
2882
|
+
const scoped = hits.filter((s) => s.enclosing === scope)
|
|
2883
|
+
if (scoped.length > 0) hits = scoped
|
|
2884
|
+
}
|
|
2885
|
+
if (hits.length === 0) {
|
|
2886
|
+
throw Object.assign(new Error(`no symbol matching "${raw}"`), { code: 'STRUCT_NOT_FOUND' })
|
|
2887
|
+
}
|
|
2888
|
+
if (ordinal !== null) {
|
|
2889
|
+
if (ordinal > hits.length) {
|
|
2890
|
+
throw Object.assign(new Error(`"${raw}" has only ${hits.length} occurrence(s)`), { code: 'STRUCT_NOT_FOUND' })
|
|
2891
|
+
}
|
|
2892
|
+
hits = [hits[ordinal - 1]]
|
|
2893
|
+
}
|
|
2894
|
+
if (hits.length === 1) return { ...hits[0], ambiguous: false }
|
|
2895
|
+
|
|
2896
|
+
if (near !== null && near !== undefined) {
|
|
2897
|
+
let best = null
|
|
2898
|
+
let bestD = Infinity
|
|
2899
|
+
let ties = 0
|
|
2900
|
+
for (const h of hits) {
|
|
2901
|
+
const d = Math.abs(h.startLine - near)
|
|
2902
|
+
if (d < bestD) {
|
|
2903
|
+
bestD = d
|
|
2904
|
+
best = h
|
|
2905
|
+
ties = 1
|
|
2906
|
+
} else if (d === bestD) ties += 1
|
|
2907
|
+
}
|
|
2908
|
+
if (ties === 1) return { ...best, ambiguous: false }
|
|
2909
|
+
}
|
|
2910
|
+
// 歧义不猜:把候选全部回报(含归属),让调用方决定。这是与 edit 的分水岭。
|
|
2911
|
+
throw Object.assign(new Error(`"${raw}" matched ${hits.length} symbols and near did not disambiguate`), {
|
|
2912
|
+
code: 'STRUCT_AMBIGUOUS',
|
|
2913
|
+
candidates: hits.map((h) => ({ name: h.name, kind: h.kind, enclosing: h.enclosing, startLine: h.startLine, endLine: h.endLine })),
|
|
2914
|
+
})
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
/**
|
|
2918
|
+
* 给一个候选区域打结构置信度:这个区域是否**恰好**对应一个完整符号(或其完整函数体)。
|
|
2919
|
+
*
|
|
2920
|
+
* 用途不是"排序文本像不像",而是回答另一个问题:**这个区域是不是一个语法上完整的单位**。
|
|
2921
|
+
* 文本相似度高的两个区域,只有一个可能是完整的函数体——这就是结构能给而文本给不了的信号。
|
|
2922
|
+
*
|
|
2923
|
+
* @returns {number} 0..1,1 表示区间与某个符号的声明跨度完全吻合
|
|
2924
|
+
*/
|
|
2925
|
+
export function regionConfidence(index, startLine, endLine) {
|
|
2926
|
+
if (index.symbols.length === 0) return 0
|
|
2927
|
+
let best = 0
|
|
2928
|
+
for (const s of index.symbols) {
|
|
2929
|
+
if (s.startLine === startLine && s.endLine === endLine) return 1
|
|
2930
|
+
const overlap = Math.max(0, Math.min(s.endLine, endLine) - Math.max(s.startLine, startLine) + 1)
|
|
2931
|
+
const span = Math.max(s.endLine - s.startLine + 1, endLine - startLine + 1)
|
|
2932
|
+
best = Math.max(best, overlap / span)
|
|
2933
|
+
}
|
|
2934
|
+
return best
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
/**
|
|
2938
|
+
* 按重要性给符号排序:**"展示哪些符号"这个决策的唯一依据**。
|
|
2939
|
+
*
|
|
2940
|
+
* 为什么排序是必需的,而不是锦上添花:展示符号的目标函数是
|
|
2941
|
+
* "最大化模型下一步能直接寻址的概率",而不是"展示最多符号"。这两个函数对排序的要求
|
|
2942
|
+
* 截然不同——后者不在乎顺序,前者必须按重要性排。而**声明顺序与重要性零相关**:
|
|
2943
|
+
* 实测按声明顺序取前 24 个,其中 49% 是函数内局部量(`variable v` 这类,重名率极高)。
|
|
2944
|
+
*
|
|
2945
|
+
* 放在本模块(而非 read 或 locator)是因为它只依赖符号表与行文本,两边都要用:
|
|
2946
|
+
* 内容侧用它决定地图展示什么,定位侧用它决定失败回执给哪些候选。
|
|
2947
|
+
* 曾经两边各有一份实现,结果只修了一处、漏了另一处——共享一份是避免那种漏改的唯一办法。
|
|
2948
|
+
*
|
|
2949
|
+
* 级别依据(全部取自现有索引,不引入新概念):
|
|
2950
|
+
* 3 导出 —— 一个文件的接口就是它的导出面,最可能被改
|
|
2951
|
+
* 2 顶层结构 —— function/class 等,其次
|
|
2952
|
+
* 1 顶层变量 / 嵌套结构
|
|
2953
|
+
* 0 函数内局部量 —— `const v = ...` 这类。**不展示**,不是因为它不重要,
|
|
2954
|
+
* 而是因为模型改它时不必看见它:它已有行号(`lines` 层),
|
|
2955
|
+
* 或用外层函数名定位即可。展示行是稀缺资源。
|
|
2956
|
+
*
|
|
2957
|
+
* @param index - indexStructure 的结果
|
|
2958
|
+
* @param lines - 文件按行切分(用于识别 `export` 与顶层性)
|
|
2959
|
+
* `exported` 必须**一并带出**,不能只留一个分数。它在这里已经算出来了(`isExported`),
|
|
2960
|
+
* 而先前的返回值把它丢掉、只给 `imp`,于是两个消费方各自用 `imp === 3` 反推"是否导出"——
|
|
2961
|
+
* 一个数字同时承担"重要性排序"与"是否导出"两种含义,是典型的双义字段:
|
|
2962
|
+
* 一旦重要性分级扩容(比如再加一级),"导出"会静默变成别的东西而没人发现。
|
|
2963
|
+
* 现在把已算出的布尔值直接给出,两个消费方都不再反推。
|
|
2964
|
+
*
|
|
2965
|
+
* @returns {{s, imp, exported}[]} 按重要性降序、同级别按行号升序
|
|
2966
|
+
*/
|
|
2967
|
+
export function rankSymbols(index, lines) {
|
|
2968
|
+
return listSymbols(index)
|
|
2969
|
+
.map((s) => {
|
|
2970
|
+
const own = lines[s.startLine - 1] ?? ''
|
|
2971
|
+
const isExported = /\bexport\b/.test(own)
|
|
2972
|
+
const top = s.enclosing === null || s.enclosing === undefined
|
|
2973
|
+
const structural =
|
|
2974
|
+
s.kind === 'function' || s.kind === 'class' || s.kind === 'type' || s.kind === 'interface' || s.kind === 'enum'
|
|
2975
|
+
let imp = 0
|
|
2976
|
+
if (top && isExported) imp = 3
|
|
2977
|
+
else if (top && structural) imp = 2
|
|
2978
|
+
else if (top || structural) imp = 1
|
|
2979
|
+
return { s, imp, exported: isExported }
|
|
2980
|
+
})
|
|
2981
|
+
.sort((a, b) => b.imp - a.imp || a.s.startLine - b.s.startLine)
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
/**
|
|
2985
|
+
* 列出符号表,供 `list` 查询(模型据此在不知道确切名字时先看目录)。
|
|
2986
|
+
*
|
|
2987
|
+
* 投影里**必须保留 `enclosing`**。曾经这里只返回 name/kind/startLine/endLine,
|
|
2988
|
+
* 结果消费方(read 的 digest 模式)拿到的 `enclosing` 恒为 `undefined`,
|
|
2989
|
+
* 于是"函数体内的局部变量不是导出"这条过滤完全失效——`let sum` 被当成导出报了出去。
|
|
2990
|
+
*
|
|
2991
|
+
* 教训:投影丢字段是一种静默失败。它不报错,只是让下游的某个判断**永远走另一条分支**,
|
|
2992
|
+
* 而且看起来一切正常。凡是可能被"归属/作用域"类判断用到的字段,都不能在投影里省。
|
|
2993
|
+
*
|
|
2994
|
+
* @param index - indexStructure 的结果
|
|
2995
|
+
* @param kind - 只列某一类符号;null 表示全部
|
|
2996
|
+
* @returns 符号投影(含 enclosing)
|
|
2997
|
+
*/
|
|
2998
|
+
export function listSymbols(index, kind = null) {
|
|
2999
|
+
return index.symbols
|
|
3000
|
+
.filter((s) => kind === null || s.kind === kind)
|
|
3001
|
+
.map((s) => ({
|
|
3002
|
+
name: s.name,
|
|
3003
|
+
kind: s.kind,
|
|
3004
|
+
startLine: s.startLine,
|
|
3005
|
+
endLine: s.endLine,
|
|
3006
|
+
enclosing: s.enclosing ?? null,
|
|
3007
|
+
// 跨度是否覆盖了符号真实占据的整段。少了它,调用方分不出"完整符号"与"只有签名行",
|
|
3008
|
+
// 而那个区别决定了 `target=` 能不能替换。
|
|
3009
|
+
spanComplete: s.spanComplete !== false,
|
|
3010
|
+
spanNote: s.spanNote ?? '',
|
|
3011
|
+
}))
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
/** 符号体内的引用位置:用于重命名全量改写这类需要"所有出现处"的操作。 */
|
|
3015
|
+
export function findReferences(index, name) {
|
|
3016
|
+
const out = []
|
|
3017
|
+
for (const s of index.symbols) {
|
|
3018
|
+
for (let k = s.startTok; k <= s.endTok; k += 1) {
|
|
3019
|
+
const t = index.tokens[k]
|
|
3020
|
+
if (t !== undefined && t.type === 'ident' && t.value === name) {
|
|
3021
|
+
out.push({ startLine: t.line, endLine: t.line, within: s.name })
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
return out
|
|
3026
|
+
}
|