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.
12 KiB
12 KiB
计划: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.vp1960/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. 目标 / 非目标
目标:
- LSP 推送与拉取诊断统一包含:语法、名称解析、位宽/存储三类问题。
- 每条诊断有稳定
code、精确 range、严重级别、可选的 relatedInformation / 快速修复。 - 规则与编译器语义一致;不一致的地方必须是有意为之并写进文档。
- 对仓库自身代码(能正常编译)零误报。
- 大文件诊断有明确时间/数量上限,不阻塞编辑。
已定决策:
- 字面量宽度后缀
_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 共用表。
规则:
literal-width-overflow:显式后缀_u8/_int8/.../_uint64与幅值不符 → error;范围[0, 2^n-1]/[-2^(n-1), 2^(n-1)-1];消息与ExprGenConst.vp:79对齐。decl-narrowing:带注解的声明/字段/全局/define 用字面量初始化且越界 → error。 只处理内建整数类型与已知别名(char/uchar/uint/int/long/sizet等);typedef、泛型、指针跳过。assign-narrowing:x = 字面量,x解析到带注解符号且越界 → error。neg-literal:-1形式;只处理一元负号 + 字面量,不做常量折叠。array-init-length:[N]T字符串初始化strlen+1 > N,或列表元素数 ≠/ > N → error (与编译器消息一致)。float-narrowing:先核对编译器对x: float = 3.5是否放行,再决定是否实现;不猜想。- 64 位以上字面量:lexer 已有溢出诊断,确保不重复上报。
验收:fixture 覆盖每个规则的正/反例;仓库自身代码零 error。
P2 NameError(2–3 天)
前置:
LspSymbols给模块路径 occurrence 打标记(is_module_path),避免把模块段当未定义名。- 建立内置名白名单(
int/bool/str/bytes/...、转换函数、len/sizeof/defined等); 优先从编译器内建表生成,避免手抄漂移。 defined(NAME)的参数不算未定义。
规则:
name-undefined:load 上下文的裸标识符,sym/head都为空,且不是内建/类型/泛型参数/ 模块路径/defined参数 → error(或首版 warning)。unimported-module-call:mod.f(...)中mod是工作区已知模块但本文件未 import → error;对齐CallGenMixin.vp:253。- 快速修复:按工作区索引找同名导出符号,提供「添加 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. 依赖与顺序
- P0 基础设施
- P1 前置:lexer 宽度后缀修复
- P1 位宽规则(与 hover 共用事实表)
- P2 NameError(先白名单/抑制,再规则)
- P3 导入诊断
- P4 语料/性能/文档
每个阶段结束:ast_check → 一次 --build-self → 对应 gate + 现有 gate1–12 → 部署 →
单独提交。不在同一阶段内反复重建。
7. 需要拍板的点
已拍板:
- 宽度表:编译器与 LSP 共用
IntFacts。 - 后缀:任意正整数 N,语言层不限制。
- LSP 位宽展示:只给二幂位宽。
- 诊断级别:Pylance 式 off/basic/standard/strict,默认 standard。
仍待确认:
- NameError 首版是否包含裸函数调用(编译器允许未知调用退化为外部 C 函数,建议不报)。
module-not-found是否默认 warning(工作区 roots 可能不完整,建议 warning)。