aura-basetools 0.7.0 → 0.13.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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * repo-map.mjs —— **仓库级粗粒度地图**(T26):`aura_read <目录>` 该回什么。
2
+ * repo-map.mjs —— **仓库级粗粒度地图**(T26):`aura_basetools_read <目录>` 该回什么。
3
3
  *
4
4
  * ## 为什么不是“文件的 map 的 map”
5
5
  *
@@ -246,7 +246,12 @@ function tokenize(src, lang) {
246
246
  // 数字
247
247
  if (/[0-9]/.test(c)) {
248
248
  const start = i
249
- while (i < n && /[0-9a-fA-FxX._]/.test(src[i])) i += 1
249
+ while (i < n && /[0-9a-fA-FxX._]/.test(src[i])) {
250
+ // Erlang 的 `.` 既是语句终结符、又可能是小数点:`1.` 是整数 1 + 终结符,
251
+ // `1.0` 才是浮点。只在 `.` 后面还有数字时把它算进数字,否则留给标点层。
252
+ if (lang.dotTerminatesNumber === true && src[i] === '.' && !/[0-9]/.test(src[i + 1] ?? '')) break
253
+ i += 1
254
+ }
250
255
  push('number', start, i)
251
256
  continue
252
257
  }
@@ -293,7 +298,7 @@ function tokenize(src, lang) {
293
298
  * 它 10 个顶层 `function` 声明里,**索引只认出 2 个**,其余 8 个凭空消失
294
299
  * (`renderChanged` `renderFull` `renderMap` `renderAround` `renderLines`
295
300
  * `renderSearch` `windowEnvelope` `fitWithin`),而已认出的 `renderDigest`
296
- * 跨度是 `69-69`(真实 69-164)。于是 `aura_edit target=<这些名字>` 全部
301
+ * 跨度是 `69-69`(真实 69-164)。于是 `aura_basetools_edit target=<这些名字>` 全部
297
302
  * `STRUCT_NOT_FOUND`,而 `map` 把这些函数报成不存在——**本工具连自己的引擎都定位不了。**
298
303
  *
299
304
  * 机制:`/` 被当成普通标点,于是
@@ -334,7 +339,7 @@ const NO_HEADER_KEYWORDS = new Set([])
334
339
  * 块体**终结规则**:`bodyOpen: null` 的语言靠它把跨度补到真实末端。
335
340
  *
336
341
  * 这是配置表里第三条语言相关的真差异(前两条是注释与字符串语法)。缺了它,跨度就只有签名行,
337
- * 而后果是**静默**的:`aura_edit target=<函数名>` 只换掉签名、旧函数体原样留下,
342
+ * 而后果是**静默**的:`aura_basetools_edit target=<函数名>` 只换掉签名、旧函数体原样留下,
338
343
  * `python -m py_compile` 仍然返回 0。因此判据在这里,与其它差异并列:
339
344
  *
340
345
  * { style: 'indent' } 缩进决定块体(Python / Cython)
@@ -356,10 +361,16 @@ const INDENT_BLOCK = { style: 'indent', headerEnds: new Set([':']) }
356
361
  * 而这里的语言压根没有配对的收尾词——SQL 只有语句终结符,Erlang 的函数子句只有一个 `.`。
357
362
  * 硬套 `closer` 要给每条规则编一个假开块词,那是在配置里写谎话。
358
363
  *
359
- * `terminators` 只列**本语言真实**的终结符:漏列只会让跨度偏短(安全方向),
360
- * 多列则会把 `Erlang 的 x.y` 这种成员访问误当语句尾(跨度偏长,危险方向)。
364
+ * `newlineStops` 默认 true:没有终结符时,换行是安全方向的兜底(SQL 的多语句、Vim 的无尾声明)。
365
+ * Erlang 关掉它:函数体的换行是普通空白,唯一合法的收尾是 `.`;开启会把多行函数体截在签名行。
366
+ * 关掉后必须在文件内找到终结符,否则跨度判为不完整(fail-closed)——见 terminatorBlockEnd。
361
367
  */
362
- const TERMINATOR_BLOCK = (terminators) => ({ style: 'terminator', terminators: new Set(terminators) })
368
+ const TERMINATOR_BLOCK = (terminators, { newlineStops = true } = {}) => ({
369
+ style: 'terminator',
370
+ terminators: new Set(terminators),
371
+ newlineStops,
372
+ })
373
+
363
374
 
364
375
  /**
365
376
  * 除 `bodyOpen` 之外还算块开符的字符:`[` —— `const X = [ … ]` 这种字面量体。
@@ -508,9 +519,9 @@ const RUST = {
508
519
  { kind: 'trait', pattern: ['$mods*', 'trait', '$name', '$param*'] },
509
520
  { kind: 'impl', pattern: ['impl', '$param*', '$name'] },
510
521
  { kind: 'mod', pattern: ['$mods*', 'mod', '$name'] },
511
- // Rust 的常量/静态绑定。`const`/`static` 已在 modifiers 里,但**规则不能带 `$mods*`**:
512
- // 那正是 `pub const R: i32 = 1` 之前一个符号都提不出来的原因——
513
- // 同 `export const` 那个洞(变量规则缺 `$mods*`)同源。
522
+ // Rust 的常量/静态绑定:`const`/`static` 本身也在 modifiers 里,规则必须带 `$mods*`,
523
+ // 否则 `pub const R` 会被“前一 token 是修饰符”的去重闸门判掉。
524
+ // `$mods*` 的逐长度回溯(matchPattern)让 `pub const` 能停在 `pub` 之后、把 `const` 留给字面量。
514
525
  { kind: 'variable', pattern: ['$mods*', 'const', '$name'], guard: 'notKeyword' },
515
526
  { kind: 'variable', pattern: ['$mods*', 'static', '$name'], guard: 'notKeyword' },
516
527
  ],
@@ -1183,7 +1194,10 @@ const ERLANG = {
1183
1194
  multiPunct: ['->', '<-', '==', '/=', '=<', '>=', '++', '--', '::', '||', '|>', '<<', '>>'],
1184
1195
  brackets: { '(': ')', '[': ']', '{': '}' },
1185
1196
  bodyOpen: null,
1186
- blockEnd: TERMINATOR_BLOCK(['.']),
1197
+ dotTerminatesNumber: true,
1198
+
1199
+ blockEnd: TERMINATOR_BLOCK(['.'], { newlineStops: false }),
1200
+
1187
1201
  rules: [
1188
1202
  // `$param` 是**跳过**一个括号组、不捕获任何名字,所以属性名必须写 `'(' '$name'`:
1189
1203
  // 用 `$param` 的结果是 `cap.$name` 为空 ⇒ 符号被 `indexStructure` 静默丢弃(实测 0 个符号)。
@@ -1611,7 +1625,7 @@ function matchAngle(tokens, open) {
1611
1625
  * `$name` 捕获符号名(必须是标识符;决定声明起点)
1612
1626
  * `$param` 跳过一个配平括号组(必选)
1613
1627
  * `$param*` 可选:有 `()` 或 `<>` 就跳,没有就跳过这一段
1614
- * `$mods*` 可选:吃掉任意个 `modifiers` 词表里的修饰符
1628
+ * `$mods*` 可选:吃掉任意个 `modifiers` 词表里的修饰符;**逐长度回溯**(字面量本身在词表里时能停在中途)
1615
1629
  * `$ident*` 可选(**非贪婪**):吃掉任意个非关键字标识符
1616
1630
  * `$rest` 跳到**当前所在括号组的收尾 token**(见 `skipToGroupClose`)
1617
1631
  *
@@ -1645,7 +1659,27 @@ function matchPattern(tokens, start, pattern, pi, cap, lang, pairs) {
1645
1659
  const part = pattern[pi]
1646
1660
  const t = tokens[start]
1647
1661
 
1648
- if (part === '$param*' || part === '$mods*' || part === '$ident*') {
1662
+ if (part === '$mods*') {
1663
+ /**
1664
+ * 修饰符段要**逐个长度回溯**,不能只试「全吃」与「不吃」。
1665
+ *
1666
+ * `pub const G: i32 = 1` 里 `const` 同时是 Rust 的 modifier 和规则字面量:
1667
+ * 全吃 ⇒ 后面的字面量 `const` 永远等不到;不吃 ⇒ 字面量对不上 `pub`。
1668
+ * 中间那一步(吃 `pub`、留 `const`)才是解。`pub const fn f` 仍由全吃那一支命中。
1669
+ */
1670
+ const stops = [start]
1671
+ let i = start
1672
+ while (tokens[i] !== undefined && tokens[i].type === 'ident' && lang.modifiers !== undefined && lang.modifiers.has(tokens[i].value)) {
1673
+ i += 1
1674
+ stops.push(i)
1675
+ }
1676
+ for (let s = stops.length - 1; s >= 0; s -= 1) {
1677
+ const hit = matchPattern(tokens, stops[s], pattern, pi + 1, cap, lang, pairs)
1678
+ if (hit !== null) return hit
1679
+ }
1680
+ return null
1681
+ }
1682
+ if (part === '$param*' || part === '$ident*') {
1649
1683
  // 先试"跳"的版本,失败再试"不跳"的版本——这就是回溯
1650
1684
  const advanced = skipOptional(tokens, start, part, lang, pairs)
1651
1685
  if (advanced !== start) {
@@ -1692,13 +1726,8 @@ function matchPattern(tokens, start, pattern, pi, cap, lang, pairs) {
1692
1726
  return matchPattern(tokens, start + 1, pattern, pi + 1, cap, lang, pairs)
1693
1727
  }
1694
1728
 
1695
- /** 可选段的"跳"这一步:跳不动就原地返回,由调用方回溯到"不跳"的分支。 */
1729
+ /** 可选段的"跳"这一步(`$param*` / `$ident*`):跳不动就原地返回,由调用方回溯。 */
1696
1730
  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
1731
  if (part === '$ident*') {
1703
1732
  // 只吃**非关键字**标识符:关键字是模式里的字面量,不该被这个段吞掉
1704
1733
  let i = k
@@ -1784,13 +1813,38 @@ function matchRule(tokens, i, rule, lang, pairs, lineAt) {
1784
1813
  */
1785
1814
  if (rule.guard === 'topLevel' && !clauseInitial(tokens, startTok, lineAt)) return null
1786
1815
  if (rule.guard === 'attributeClause') {
1787
- // 判据要落在**规则起点**的前一个 token(`-`),不是名字的前一个:
1788
- // `['-', 'define', '(', '$name', …]` 里名字前面是 `(`,拿它去比 `-` 永远不成立,
1789
- // 于是同一个 `-define(ALPHA, 1)` 会被属性规则又命中一次(实测报出假符号 `define`)。
1816
+ /**
1817
+ * Erlang 函数子句是**语句级**声明:只能出现在文件开头、上一语句的 `.` 之后,
1818
+ * 或同一函数上一子句的 `;` 之后。
1819
+ *
1820
+ * 这条边界同时挡住三类假符号:属性里的名字(`-module(alpha)` / `-define(ALPHA, …)`)、
1821
+ * `-spec f(A) -> …` 里的 `f`(spec 内部也有 `->`,只看箭头会误收)、以及函数体里的
1822
+ * 调用 `foo(X)`(前一 token 是 `(` / `,` / `=` 等)。
1823
+ *
1824
+ * 前一 token 是 `;` 表示同一函数的后续子句:它们共享到最后一个 `.` 的跨度,
1825
+ * 重复登记会让 `target=f` 报 STRUCT_AMBIGUOUS,所以只登记第一个子句。
1826
+ */
1790
1827
  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
1828
+ if (prev !== undefined) {
1829
+ if (prev.type !== 'punct' || (prev.value !== '.' && prev.value !== ';')) return null
1830
+ if (prev.value === ';') return null
1831
+ }
1832
+ const open = tokens[startTok + 1]
1833
+ if (open === undefined || open.type !== 'punct' || open.value !== '(' || !pairs.has(startTok + 1)) return null
1834
+ // 参数组之后可以是 `->`,也可以先是 `when` 守卫(`f(X) when X > 0 -> …`)。
1835
+ // 守卫里没有 `->`;遇到 `;` / `.` 仍没有 `->` 说明这不是函数子句。
1836
+ let j = end
1837
+ for (; j < tokens.length; j += 1) {
1838
+ const t = tokens[j]
1839
+ if (t.type !== 'punct') continue
1840
+ if (t.value === ';' || t.value === '.') return null
1841
+ if (pairs.has(j)) {
1842
+ j = pairs.get(j)
1843
+ continue
1844
+ }
1845
+ if (t.value === '->') break
1846
+ }
1847
+ if (j >= tokens.length) return null
1794
1848
  }
1795
1849
  // 放最后,因为它与其它守卫互不依赖。
1796
1850
  if (looksLikeBinding(tokens, startTok, k, lang)) return null
@@ -2107,7 +2161,7 @@ function isTerminatorToken(t, terminators) {
2107
2161
  * {@link terminatorBlockEnd} 用的两张后缀最小值表。判定优先级与逐 token 扫描同一个 k 上的次序相同。
2108
2162
  * 只在该语言真的走终结符风格时才会被调用。
2109
2163
  */
2110
- function terminatorScan(tokens, pairs, lineAt, terminators) {
2164
+ function terminatorScan(tokens, pairs, lineAt, terminators, newlineStops) {
2111
2165
  const n = tokens.length
2112
2166
  const nextStop = new Int32Array(n)
2113
2167
  const nextPairOpen = new Int32Array(n)
@@ -2132,7 +2186,8 @@ function terminatorScan(tokens, pairs, lineAt, terminators) {
2132
2186
  chainEnd[i] = after < n && nextPairOpen[after] !== -1 ? chainEnd[nextPairOpen[after]] : i
2133
2187
  }
2134
2188
  // 终结符优先于括号跳层,括号跳层优先于换行 —— 与原循环在同一个 k 上的判定次序一一对应
2135
- if (isTerminatorToken(t, terminators) || (!isOpen && i >= 1 && lineAt(t.start) > lineAt(tokens[i - 1].end))) {
2189
+ // 终结符优先于括号跳层,括号跳层优先于换行(newlineStops=false 时换行不是停止条件)
2190
+ if (isTerminatorToken(t, terminators) || (newlineStops && !isOpen && i >= 1 && lineAt(t.start) > lineAt(tokens[i - 1].end))) {
2136
2191
  stop = i
2137
2192
  }
2138
2193
  nextStop[i] = stop
@@ -2148,8 +2203,8 @@ function terminatorScan(tokens, pairs, lineAt, terminators) {
2148
2203
  */
2149
2204
  function lazyTerminatorScan(tokens, pairs, lineAt) {
2150
2205
  let table = null
2151
- return (terminators) => {
2152
- if (table === null) table = terminatorScan(tokens, pairs, lineAt, terminators)
2206
+ return (spec) => {
2207
+ if (table === null) table = terminatorScan(tokens, pairs, lineAt, spec.terminators, spec.newlineStops !== false)
2153
2208
  return table
2154
2209
  }
2155
2210
  }
@@ -2160,6 +2215,8 @@ function lazyTerminatorScan(tokens, pairs, lineAt) {
2160
2215
  * 判据与跳表分别落在 {@link isTerminatorToken} 与 {@link terminatorScan} 上)。
2161
2216
  */
2162
2217
  function terminatorBlockEnd(tokens, pairs, fromTok, spec, lineAt, scan) {
2218
+ // 只认终结符(Erlang):找不到就是跨度不完整,不能拿换行/组尾兜底当一个完整跨度。
2219
+ const onlyTerminator = spec.newlineStops === false
2163
2220
  let skippedEnd = -1
2164
2221
  let k = fromTok + 1
2165
2222
  if (k >= tokens.length) return { endTok: -1, note: '' }
@@ -2177,15 +2234,16 @@ function terminatorBlockEnd(tokens, pairs, fromTok, spec, lineAt, scan) {
2177
2234
  if (s !== -1 && (o === -1 || s < o)) {
2178
2235
  // stop 位置一定不是括号开符(建表时就排除了),所以 `s === o` 不可能出现
2179
2236
  if (isTerminatorToken(tokens[s], spec.terminators)) return { endTok: s, note: '' }
2237
+ if (onlyTerminator) return { endTok: -1, note: '' }
2180
2238
  return { endTok: skippedEnd === -1 ? s - 1 : skippedEnd, note: '' }
2181
2239
  }
2182
- if (o === -1) return { endTok: skippedEnd, note: '' }
2240
+ if (o === -1) return { endTok: onlyTerminator ? -1 : skippedEnd, note: '' }
2183
2241
  // 这一段里一个停止条件都没有:原循环会一路跨到**最后一个**组尾 ⇒ 用链尾表一次到位(O(1))
2184
- if (s === -1) return { endTok: pairs.get(chainEndFrom[k]), note: '' }
2242
+ if (s === -1) return { endTok: onlyTerminator ? -1 : pairs.get(chainEndFrom[k]), note: '' }
2185
2243
  skippedEnd = pairs.get(o)
2186
2244
  k = skippedEnd + 1
2187
2245
  }
2188
- return { endTok: skippedEnd, note: '' }
2246
+ return { endTok: onlyTerminator ? -1 : skippedEnd, note: '' }
2189
2247
  }
2190
2248
 
2191
2249
  /**
@@ -2324,7 +2382,7 @@ function headerContinues(tokens, k, prev, clauses) {
2324
2382
  * (`bodyOpen` 或 `blockEnd`),扫描器里不写语言分支。
2325
2383
  *
2326
2384
  * **跨度必须覆盖符号真实占据的整段**:拿不到完整跨度时不是退化成签名行,而是标记
2327
- * `spanComplete: false` —— 调用方(`locate`,进而 `aura_edit`)据此刻拒绝 `target=` 替换。
2385
+ * `spanComplete: false` —— 调用方(`locate`,进而 `aura_basetools_edit`)据此刻拒绝 `target=` 替换。
2328
2386
  * 理由是实测的:偏短的跨度会让"只换签名行"这件事**静默**发生,落盘之后语法检查毫无反应。
2329
2387
  *
2330
2388
  * 这里还要分辨一件事:`const describe = (x) => { ... }` 是**函数绑定**,不是普通变量。
@@ -2378,7 +2436,7 @@ function declarationSpan(tokens, pairs, startTok, endTok, lang, lineAt, kind, li
2378
2436
  spanNote = hit.note
2379
2437
  } else last = hit.endTok
2380
2438
  } else if (spec.style === 'terminator') {
2381
- const hit = terminatorBlockEnd(tokens, pairs, endTok, spec, lineAt, termScan(spec.terminators))
2439
+ const hit = terminatorBlockEnd(tokens, pairs, endTok, spec, lineAt, termScan(spec))
2382
2440
  if (hit.endTok === -1) {
2383
2441
  spanComplete = false
2384
2442
  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`
@@ -2410,7 +2468,7 @@ function declarationSpan(tokens, pairs, startTok, endTok, lang, lineAt, kind, li
2410
2468
 
2411
2469
  // ── 结构索引的记忆化 ───────────────────────────────────────────────────────
2412
2470
  /**
2413
- * `indexStructure` 是热路径:`aura_read` 的每个结构化模式、`aura_edit` 每次结构化编辑都会建一次
2471
+ * `indexStructure` 是热路径:`aura_basetools_read` 的每个结构化模式、`aura_basetools_edit` 每次结构化编辑都会建一次
2414
2472
  * 完整索引(token 流 + 括号配平 + 符号表)。实测本仓最大的引擎文件(本文件,施工前 97 KB /
2415
2473
  * 2573 行)建一次 **15.8 ms**,而同一份内容在一轮里常被连问几次(read 的 map → digest → target
2416
2474
  * 各一次,edit 的 plan 与 verify 各一次)——同一份工作被反复做。这一层把它记住。