Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions TeXmacs/plugins/goldfish/src/s7.c
Original file line number Diff line number Diff line change
Expand Up @@ -59953,6 +59953,10 @@ static no_return void unbound_variable_error_nr(s7_scheme *sc, s7_pointer sym)
#endif
for (int32_t i = start; i < end; i++)
{
/* main_names has only 430 entries but main_names_index is sized
for 443, so the length-20..23 buckets run into trailing NULL
slots; skip NULLs to avoid dereferencing them in levenshtein. */
if (main_names[i] == NULL) continue;
int32_t diff = levenshtein(sc, sym_name, sym_len, main_names[i], sym_len) + 1; /* perhaps same but i+/-1 as well */
if (sym_name[0] != main_names[i][0]) diff++;
if (diff < min_diff)
Expand Down
66 changes: 66 additions & 0 deletions devel/2031.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# [2031] 修复切主题时 s7 "did you mean" 建议解引用 NULL 导致 SIGSEGV

## 1 相关文档
- [dddd.md](dddd.md) - 任务文档模板
- [2030.md](2030.md) - QML 弹窗系统(同期主题相关改动)

## 2 任务相关的代码文件
- `TeXmacs/plugins/goldfish/src/s7.c` — `unbound_variable_error_nr()` 内的"did you mean"建议表(`main_names` / `main_names_index`)

## 3 如何测试

### 3.1 复现(修复前)
切主题会触发未绑定变量 `set-pretty-preference*`,s7 在报错前为其计算"did you mean"建议,遍历到 NULL 解引用崩溃:
```bash
xmake b stem
xmake r stem # 启动后在菜单里切换主题 → SIGSEGV
```

### 3.2 验证(修复后)
切主题不再崩溃,且仍能给出合理的拼写建议:
```bash
xmake b stem
xmake r stem # 切换主题正常,无段错误
```

## 4 如何提交

```bash
gf fmt --changed-since=main # 格式化本次改动的 .cpp/.hpp/.scm
```

## 5 What
1. 在 `unbound_variable_error_nr()` 遍历 `main_names` 的循环里,对取到的表项增加 NULL 判空,遇到 NULL 直接 `continue`,避免把它传给 `levenshtein()` 解引用。

## 6 Why

这是 commit `7daf2e6d7`([2004] 把 `s7.c` 从 `3rdparty/s7/` 迁到 `goldfish/src/`)新引入的 regression。

s7 在报"未绑定变量"错误前,会按变量名的字符长度分桶,用 levenshtein 距离在一张内置符号表 `main_names` 里找最接近的拼写建议("did you mean …")。桶的边界由 `main_names_index` 给出,`sym_len` 长度的桶覆盖区间 `[main_names_index[sym_len-2], main_names_index[sym_len-1])`。

旧版(`3rdparty/s7/`)的 `main_names` 填满了 `MAIN_NAMES_SIZE`(445 项全有效),所有桶都落在有效区。新版 `goldfish/src/` 重构时删减/重排了这张表,**实际只填了 430 条**(索引 0–429 有效,430–442 是 NULL),但:

- `MAIN_NAMES_SIZE` 仍是 **443**(`#define MAIN_NAMES_SIZE 443`,`main_names[443]` 后 13 个槽位为零初始化的 NULL 指针);
- `main_names_index` 仍按"填满 443"计算,末尾几个桶的边界被 NULL 区覆盖。

逐桶核对(实际 430 条有效,索引 ≥430 为 NULL):

| sym_len | 桶区间 | 状态 |
|---------|--------|------|
| 2 … 19 | 全落在 [0, 425) | 全部有效 |
| **20** | `[425, 432)` | **跨越边界**:425–429 有效,430–431 为 NULL |
| **21** | `[432, 435)` | 完全落在 NULL 区 |
| **22** | `[435, 440)` | 完全落在 NULL 区 |
| **23** | `[440, 442)` | 完全落在 NULL 区 |

切主题时调用的 `set-pretty-preference*` 恰为 **22 字符**,落在完全为 NULL 的桶 `[435, 440)`,遍历第一项就取到 NULL。`levenshtein()` 内部对 NULL 字符串解引用 → SIGSEGV。

> 备注:用户最初印象是 20 字符,实际 `set-pretty-preference*` 为 22 字符;但触发条件是 20–23 这几个桶整体落入 NULL 区,结论不变。

## 7 How
修复采用最小侵入的"判空跳过":在遍历循环开头加一行 `if (main_names[i] == NULL) continue;`。

之所以选这个方案而非"把 `MAIN_NAMES_SIZE` / `main_names_index` 改成与 430 严格一致":
- s7 是 vendored 的第三方单文件(4 万+ 行),最小改动便于后续跟踪上游;
- `main_names` 由脚本生成,精确重算 22 个桶边界易引入新的对齐错误,而 NULL 跳过对任何"表未填满"的情况都鲁棒;
- 不影响建议质量——NULL 槽位本就不含符号,跳过它们等价于从未定义过这些槽。
Loading