Files
ViperC/Tools/lsp/PLAN_diagnostics.md
T
TermiNexus fde6571a3b feat(lsp): 类型赋值/调用/隐式转换诊断对齐编译器
Adds a conservative type model (LspTypes) and checks for annotated declarations, plain assignments to annotated names, and same-file function calls: implicit narrowing (including the compiler's constant bit-range exemption), bool narrowing, double->float, int<->ptr, float<->ptr, scalar->aggregate, str mismatch, plus argument count/type checks. Signatures are parsed from the existing symbol detail; cross-file calls are intentionally skipped to keep publish latency low. Diagnostics run in a scratch arena sized for analysis, with a 256-diagnostic cap; gate13 covers the new rules and the repo corpus stays clean.
2026-10-28 18:56:14 +08:00

12 KiB
Raw Blame History

计划:LSP 语义诊断(NameError、位宽存储等)

状态:待评审;基础设施尚未开工。 已完成的前置工作(2026-10-28):

  • lexer 宽度后缀修复:_u8 / _i7 / _uint100 / _int24 等现在可被识别, 数字分隔符 _ 只有在后面紧跟合法数字时才被吞掉,宽度 N 不再限制在 1..64。
  • _iN/_uN 负字面量折叠:-64_i7、-128_i8 按负值做范围检查,不再被当成正的幅值拒绝。
  • atoll / atof:支持 0b/0o 前缀和 _ 数字分隔符,字面量值不再被解析成 0 或截断。
  • 新增 App/lib/types/IntFacts.vp:编译器与 LSP 共用的宽度/范围/最小二幂宽度表; ExprGenConst 的显式宽度检查、AssignGenMixin.define_min_width、hover 已改为调用它。
  • hover:_u100 等任意后缀可显示;推荐宽度仍只给二幂位宽(u8/u16/...)。
  • 类型赋值/调用/隐式转换子集:AnnAssign/Assign 的标量赋值、函数调用的参数数量与参数类型, 规则对齐 ExprGenBase.coerce_to_type(窄化、bool、double→float、int↔ptr、scalar→aggregate)。 当前只在同文件解析签名;跨文件调用暂不检查,避免 publish 时构建工作区索引。

尚未包含:任意宽度类型标注(x: i100)仍走固定类型表;这涉及 TypeGen/TypeSpec/ sizeof/ABI,需要单独评估。

1. 现状

  • LSP 目前的诊断只有语法错误:LspDiagnostics._Parse() 调用 ast.parse_ex,把 ast.get_parse_errors() 转成 textDocument/publishDiagnostics / pull diagnostics。
  • 编译器已经有成熟的报错,但都在代码生成阶段 fatal,不能直接给 LSP 复用:
    • 整数宽度:AssignGenMixin.define_min_width()(8/16/32/64)、require_cdefine_storable();
    • 字面量宽度后缀:ExprGenConst.vp "integer literal ... does not fit the explicit width";
    • 常量窄化:ExprGenBase.vp "narrowing truncation overflow" / "implicit narrowing is forbidden";
    • 数组/字符串初始化长度:AssignGenMixin.vp 1960/1970;
    • NameError:未导入模块的调用 CallGenMixin.vp:253、static 跨模块、未注册全局变量等。
  • LspSymbols.Analyze() 已经提供作用域、声明、occurrence、import/wildcard、注解详情, hover/跳转/重命名都建立在它上面;语义诊断可以复用,不需要另写一遍解析。
  • cif/celif/celse 没有独立 AST 节点:parser 把它们 desugar 成普通 If, 所以 LSP 会把所有分支的声明合并进同一作用域。这直接影响 NameError 的误报策略。
  • 已知缺陷:数字字面量宽度后缀(128_u8/128_int8)lexer 实际不识别—— _read_number 的数字循环先把 _ 当分隔符吃掉,_read_width_suffix 再看时已经没有下划线。 编译器里对应的宽度后缀检查因此是死代码。

2. 目标 / 非目标

目标:

  1. LSP 推送与拉取诊断统一包含:语法、名称解析、位宽/存储三类问题。
  2. 每条诊断有稳定 code、精确 range、严重级别、可选的 relatedInformation / 快速修复。
  3. 规则与编译器语义一致;不一致的地方必须是有意为之并写进文档。
  4. 对仓库自身代码(能正常编译)零误报。
  5. 大文件诊断有明确时间/数量上限,不阻塞编辑。

已定决策:

  • 字面量宽度后缀 _iN/_uN/_intN/_uintN 接受任意正整数 N(LLVM/MLIR 任意位宽), 语言层不设 1..64 限制;仅 int 溢出时视为非法后缀。AST 值域仍是 64 位。
  • 编译器与 LSP 共用 App/lib/types/IntFacts.vp(宽度/范围/最小二幂宽度/最小可存宽度)。
  • LSP 展示层只给二幂位宽(u8/u16/.../u128/...),不推荐任意 iN; 源里显式写出的 _u100 属于用户声明,原样回显。
  • 诊断级别采用 Pylance 式多模式:off / basic / standard / strict,默认 standard(普通)。 确定规则(字面量越界、数组长度)在 basic/standard/strict 下都是 error; 启发式规则(NameError/import)在 basic 下关闭、standard 下 warning、strict 下 error。 模式来源:initializationOptions.typeCheckingMode 与 workspace/didChangeConfiguration 的 settings.viper.typeCheckingMode(兼容 Pylance 命名)。

非目标:

  • 不实现完整类型检查器、生命周期/借用检查、泛型推断。
  • 不在每次击键时调用编译器;LSP 诊断是独立子集,不替代编译错误。
  • 不把 Vipeye 引入服务端。
  • 不实现大于 64 位字面量数值的大整数解析(N 只决定类型宽度,值仍限 64 位)。

3. 架构

新增 ViperC/App/lib/lsp/LspChecks.vp:

LspDiag { sl, sc, el, ec, severity, code, message, related_line, related_file }
Check(pool, doc, a, ws, out) -> int   # 规则调度;每条规则 append
  • LspDiagnostics 的 _BuildItems 改为「语法诊断 + 语义诊断」合并, publish 与 pull 共用同一条路径。
  • 去重键 (code, start, end);总量上限 500;超限追加一条提示并可配置关闭。
  • 每条诊断带 code,例如 name-undefined / literal-width-overflow / decl-narrowing / array-init-length / module-not-found / missing-import-member。
  • 规则按严重级别:确定结论用 error,启发式先用 warning(见决策)。
  • 缓存:按文档 version 缓存 LspAnalysis + 诊断结果,push/pull 不重复分析。
  • 预算:规则循环内检查计数与耗时(os.clock 或现有计时设施),超预算只返回已完成部分。

宽度事实表:

  • 纯逻辑模块 App/lib/Types/IntFacts.vp(不依赖 lsp/core 任一侧):
    • 类型名 → {bits, signed, min, max}
    • min_storable_width(value)(与 define_min_width 对齐:8/16/32/64)
    • 字面量 → 无符号幅值 + 后缀宽度
    • 浮点可表示性(与 hover 的 f16/f32/f64 判定共用)
  • 由编译器和 LSP 共同 import(推荐),或 LSP 先本地实现 + parity 测试(见决策)。
  • hover 的 uint/int/float 行改为调用同一张表,保证 hover 与诊断数字一致。

4. 分阶段

P0 诊断基础设施(0.5–1 天)

  • 定义 LspDiag、JSON 构造、合并、去重、code/severity/related。
  • 接入 LspDiagnostics.Publish/Pull;语法诊断仍走原路径。
  • 新增 probe.py --gate13:先放一条确定性规则(字面量宽度后缀)做端到端验证。
  • 验收:gate13 能收到带 code/range 的诊断;gate1–12 不回归。

P1 位宽与存储(1–2 天)

前置(已完成):修 includes/ast/lexer.vp 的宽度后缀;新增 IntFacts 共用表。

规则:

  1. literal-width-overflow:显式后缀 _u8/_int8/.../_uint64 与幅值不符 → error;范围 [0, 2^n-1] / [-2^(n-1), 2^(n-1)-1];消息与 ExprGenConst.vp:79 对齐。
  2. decl-narrowing:带注解的声明/字段/全局/define 用字面量初始化且越界 → error。 只处理内建整数类型与已知别名(char/uchar/uint/int/long/sizet 等);typedef、泛型、指针跳过。
  3. assign-narrowing:x = 字面量,x 解析到带注解符号且越界 → error。
  4. neg-literal:-1 形式;只处理一元负号 + 字面量,不做常量折叠。
  5. array-init-length:[N]T 字符串初始化 strlen+1 > N,或列表元素数 ≠/ > N → error (与编译器消息一致)。
  6. float-narrowing:先核对编译器对 x: float = 3.5 是否放行,再决定是否实现;不猜想。
  7. 64 位以上字面量:lexer 已有溢出诊断,确保不重复上报。

验收:fixture 覆盖每个规则的正/反例;仓库自身代码零 error。

P2 NameError(2–3 天)

前置:

  • LspSymbols 给模块路径 occurrence 打标记(is_module_path),避免把模块段当未定义名。
  • 建立内置名白名单(int/bool/str/bytes/...、转换函数、len/sizeof/defined 等); 优先从编译器内建表生成,避免手抄漂移。
  • defined(NAME) 的参数不算未定义。

规则:

  1. name-undefined:load 上下文的裸标识符,sym/head 都为空,且不是内建/类型/泛型参数/ 模块路径/defined 参数 → error(或首版 warning)。
  2. unimported-module-call:mod.f(...) 中 mod 是工作区已知模块但本文件未 import → error;对齐 CallGenMixin.vp:253。
  3. 快速修复:按工作区索引找同名导出符号,提供「添加 import」;最多 3 条 did-you-mean。

抑制规则(必须先实现,否则误报会失控):

  • 文件存在无法解析的 from X import *:该文件跳过 name-undefined。
  • cif/celif/celse:按所有分支的并集解析;任一分支绑定过就不报。
  • 无法判定的 cif 条件:跳过整条链。
  • 标识符位于属性链(a.b 的 b)→ 交给成员解析,不报 name-undefined。
  • 调用目标:编译器允许未知名字退化成外部 C 函数,v1 默认不报裸调用; 只报已知模块未导入的调用(见决策 1)。
  • 泛型参数、self/cls、c. 内建调用跳过。

暂不做:use-before-assignment、未使用变量/导入(后续独立规则)。

P3 导入/模块诊断(1–2 天)

  • module-not-found:import/from 解析不到文件且无 wildcard 兜底 → warning (工作区 roots 可能不完整,先用 warning;gate10 的 ghost_lib 夹具改为预期 warning)。
  • missing-import-member:模块存在但 from mod import name 不存在;含 __init__ 重导出、 wildcard 链、static 跨模块规则。
  • import-cycle:wildcard 重导出环 → warning(现在的 hop 上限只保证不挂死)。

P4 质量与交付(1–2 天)

  • gate13 固化:每个 code 一条正例 + 一条反例,断言 code/range/severity/message 关键字。
  • 误报语料:对 App/main.vp、App/lib/lsp/*.vp、includes/_io.vp 等能编译的文件跑 pull, 断言 0 error;脚本化进 gate13。
  • 性能:2k 行文件 pull p95 ≤ 30 ms;10k 行有预算截断;重复 pull 命中缓存。
  • 文档:Tools/lsp/README 增加诊断 code 表、抑制条件、已知差异。
  • 可选的 code action:加 import、加宽类型、显式 cast。

5. 风险与对策

风险 对策
误报摧毁信任 启发式先 warning;语料零误报门禁;每条规则可单独关闭
LSP 与编译器规则漂移 共享 IntFacts,或 parity fixture 双向对比
cif 双分支造成 NameError 假阳/假阴 分支并集 + 无法判定就跳过
wildcard 未知导出 有未解析 wildcard 时禁用 name-undefined
大文件诊断慢 缓存 + 预算 + 上限;只在打开/变更的文档上跑
索引未就绪导致跨文件误报 诊断前确保索引可用;解析失败时不产生跨文件结论
编译器允许隐式 extern 裸调用名默认不报,只报已知模块未导入

6. 依赖与顺序

  1. P0 基础设施
  2. P1 前置:lexer 宽度后缀修复
  3. P1 位宽规则(与 hover 共用事实表)
  4. P2 NameError(先白名单/抑制,再规则)
  5. P3 导入诊断
  6. P4 语料/性能/文档

每个阶段结束:ast_check → 一次 --build-self → 对应 gate + 现有 gate1–12 → 部署 → 单独提交。不在同一阶段内反复重建。

7. 需要拍板的点

已拍板:

  1. 宽度表:编译器与 LSP 共用 IntFacts。
  2. 后缀:任意正整数 N,语言层不限制。
  3. LSP 位宽展示:只给二幂位宽。
  4. 诊断级别:Pylance 式 off/basic/standard/strict,默认 standard。

仍待确认:

  1. NameError 首版是否包含裸函数调用(编译器允许未知调用退化为外部 C 函数,建议不报)。
  2. module-not-found 是否默认 warning(工作区 roots 可能不完整,建议 warning)。