fix(vipeye-internal): 支持注解内 static 前缀;*args 在未 import t 时仍是 va_list 指针

- `x: static T = v`(ViperOS VKernel/Kernel/drivers/** 全仓写法,如
  `buf_head: static uint32 = 0`)脱糖为与语句起始形态相同的 `t.CStatic | T`,
  并在声明节点打 isViperStatic,跨模块取参检查同判据;此前 `static` 在注解里
  被当普通名引用,误报「"static" is not defined」。软关键字边界不变
  (`static: int` / `static = 1` 仍按标识符),tmLanguage 一并高亮注解内 static
- `getViperCPtrType` 增加 builtins `rawptr` 回退:文件没有 `import t` 时
  `*args` 不再退回 Python tuple 语义,`viperlib.vsnprintf(&buf, 1024, fmt, args)`
  不再误报「tuple 不能赋给参数 ap(str)」(与 T* 注解 / [SIZE]T 同机制)
- 回归用例 +3:注解内 static 解析/展示/跨模块拦截 + 软关键字边界、
  未 import t 的 *args 透传;jest 805 -> 808
This commit is contained in:
TermiNexus committed 2026-09-27 14:26:42 +08:00
1 parent 2a7757e3b7
commit 5808a76b23
7 files changed
+238 -3

No files matched your search

+1
View File
@@ -154,6 +154,7 @@ packages/vipeye-internal/src/tests/__viper_type_binop_bound__/
packages/vipeye-internal/src/tests/__viper_const_ptr_deref__/
packages/vipeye-internal/src/tests/__viper_const_ptr_index__/
packages/vipeye-internal/src/tests/__viper_const_ptr_iter__/
packages/vipeye-internal/src/tests/__viper_variadic_noimport__/
# perfCompare.py cached commit bundles
/build/perfCompare/
@@ -8746,6 +8746,22 @@ export function createTypeEvaluator(
}
}
}
// 回退:builtins stub 的编译器内置 `rawptr`(= t.CPtr,见 builtins.vpi)。
// 文件没 `import t` 时(如 ViperOS 的 serial.vp)也要能用 `*args` 的
// va_list 指针语义,与 `T*` 注解 / `[SIZE]T` 一致(不强制源码 import t)。
const rawptrSymbolWithScope = lookUpSymbolRecursive(node, 'rawptr', /* honorCodeFlow */ true);
if (rawptrSymbolWithScope) {
const rawptrType = getEffectiveTypeOfSymbolForUsage(
rawptrSymbolWithScope.symbol,
/* usageNode */ undefined,
/* useLastDecl */ true
).type;
if (rawptrType && isInstantiableClass(rawptrType)) {
return convertToInstance(ClassType.specialize(rawptrType, [UnknownType.create()]));
}
}
return undefined;
}
+68 -1
View File
@@ -257,6 +257,10 @@ export class Parser {
// 不因 `*` 之后是行尾还是下一行内容而在两条解析路径间摇摆
// (见 _isAtViperPointerTypeSuffix 对换行的处理)。
private _isParsingViperTypeDescriptorInner = false;
// Viper 扩展:本次注解解析里消费过 `static` 类型描述符(`x: static T = v`)。
// 注解解析结束后由 _parseExpressionStatement 读取一次并清零:命中时把该声明
// 标为 static 符号(isViperStatic),供「禁止跨模块取参」检查使用。
private _viperStaticDescriptorInAnnotation = false;
// Viper 扩展:当前所处的类声明形态,用于校验 vtable 修饰方法的出现位置:
// `virtual def` 仅允许位于 class / virtual class 内;
// `novirtual def` 允许位于 class / virtual class / novirtual class 内。
@@ -5121,8 +5125,18 @@ export class Parser {
if (this._consumeTokenIfType(TokenType.Colon)) {
// 保留原始赋值目标(NameNode),后续 `*=` 拆分重包装时避免嵌套 TypeAnnotationNode。
const targetExpr = leftExpr;
// Viper 扩展:注解内 `static` 描述符(`x: static T = v`)→ 该声明是 static 符号。
this._viperStaticDescriptorInAnnotation = false;
annotationExpr = this._parseTypeAnnotation();
leftExpr = TypeAnnotationNode.create(targetExpr, annotationExpr);
const annotationNode = TypeAnnotationNode.create(targetExpr, annotationExpr);
leftExpr = annotationNode;
if (this._viperStaticDescriptorInAnnotation) {
annotationNode.d.isViperStatic = true;
if (targetExpr.nodeType === ParseNodeType.Name) {
targetExpr.d.isViperStatic = true;
}
this._viperStaticDescriptorInAnnotation = false;
}
if (
!this._parseOptions.isStubFile &&
@@ -5367,6 +5381,10 @@ export class Parser {
if (attributeNode) {
return attributeNode;
}
const staticNode = this._parseViperStaticTypeDescriptorPrefix();
if (staticNode) {
return staticNode;
}
const descriptorNode = this._parseViperTypeDescriptorPrefix();
if (descriptorNode) {
return descriptorNode;
@@ -5374,6 +5392,55 @@ export class Parser {
return this._parseViperBracketArrayTypeIfNeeded();
}
// Viper 扩展:注解位置的 `static` 类型描述符前缀 `x: static T = v`。
// 与语句起始形态 `static x: T = v`(见 _parseViperStaticAnnotatedStatement)
// 脱糖为同一形状 `t.CStatic | T`(存储类标记:TypeSet 语义里只作修饰,
// 不改变数据类型;展示为 `static T`)。ViperOS 全仓写法都是注解内前缀,
// 故两种位置都支持。
//
// `static` 是**软关键字**:只有「后随类型原子起点」时才在此消费,
// 其余(`x: static`、`def f(static: int)`、`self.static`、`x: static.y`)
// 仍按普通标识符处理(与 TPC `_IsStaticModifier` 的软关键字边界同口径)。
// 消费成功后置 _viperStaticDescriptorInAnnotation,供 _parseExpressionStatement
// 把该变量声明标为 static 符号(禁止跨模块取参)。
private _parseViperStaticTypeDescriptorPrefix(): ExpressionNode | undefined {
if (this._parseOptions.dialect !== 'viper') {
return undefined;
}
const staticToken = this._peekToken();
if (staticToken.type !== TokenType.Identifier) {
return undefined;
}
if (this._fileContents!.slice(staticToken.start, staticToken.start + staticToken.length) !== 'static') {
return undefined;
}
if (
!this._isViperTypeAtomStartToken(this._peekToken(1)) &&
!this._isAtViperAttributePrefix(1)
) {
return undefined;
}
this._getNextToken(); // 消费 static
// 同 const / volatile:解析被修饰的主体期间禁止就地消费指针后缀 `*`,
// 使 `static int*` 恒为 `(CStatic | int)*`。
const wasDescriptorInner = this._isParsingViperTypeDescriptorInner;
this._isParsingViperTypeDescriptorInner = true;
const innerType = this._parseViperTypeAnnotationCore();
this._isParsingViperTypeDescriptorInner = wasDescriptorInner;
this._viperStaticDescriptorInAnnotation = true;
return BinaryOperationNode.create(
this._createViperStaticMarker(staticToken.start),
innerType,
this._createViperSyntheticBarToken(staticToken),
OperatorType.BitwiseOr
);
}
// Viper 扩展:注解位置的 `__attribute__(...)` 修饰器前缀。
// 解析层脱糖为等价的联合类型注解,与手写 `T | t.attr.section(".x")` 的 AST 同形
// (编译器按 AST 扫描属性:TPC HandlesAssign._ExtractGlobalAttrs /
@@ -263,3 +263,89 @@ def f(static: int) -> int:
require('fs').rmSync(testRoot, { recursive: true, force: true });
}
});
// ViperOS 全仓实际写法是**注解内前缀**(`buf_head: static uint32 = 0`,
// 见 VKernel/Kernel/drivers/**),此前 LSP 只认语句起始形态 `static x: T`,
// 导致 `static` 在注解里被当普通名引用 → 误报「"static" is not defined」。
test('注解内 static 前缀(x: static T = v)解析、展示与跨模块拦截', () => {
const { program, configOptions, uris, testRoot, serviceProvider } = buildProgram({
'lib.vp': `buf_head: static int = 0
def touch() -> int:
return buf_head
`,
'main.vp': `import lib
def use() -> int:
return lib.buf_head
`,
});
try {
const libDiags = getDiagnostics(program, configOptions, uris['lib.vp']);
console.log('=== 注解内 static 诊断 ===');
libDiags.forEach((d) => console.log(` L${d.line} [category=${d.category}] ${d.message}`));
assert.strictEqual(
libDiags.filter((d) => d.category === 0).length,
0,
`注解内 static 不应报语法/未定义错误:${JSON.stringify(libDiags)}`
);
assert.strictEqual(
libDiags.filter((d) => d.message.includes('static 符号')).length,
0,
`同模块自引用不应报 static 跨模块错误:${JSON.stringify(libDiags)}`
);
// 注解脱糖为 `t.CStatic | int`,与语句起始形态一样展示为 `static int`。
const types = collectAnnotatedVarTypes(program, uris['lib.vp']);
console.log('=== 注解变量类型 ===');
types.forEach((v, k) => console.log(` ${k}: ${v}`));
assert(
types.get('buf_head')!.includes('static') && types.get('buf_head')!.includes('int'),
`buf_head 应展示为 static int,实际: ${types.get('buf_head')}`
);
// 跨模块取参仍须报错(与语句起始形态同判据)。
const mainDiags = getDiagnostics(program, configOptions, uris['main.vp']);
console.log('=== 跨模块取参诊断 ===');
mainDiags.forEach((d) => console.log(` L${d.line} [category=${d.category}] ${d.message}`));
const staticDiags = mainDiags.filter((d) => d.message.includes('static 符号'));
assert.strictEqual(
staticDiags.length,
1,
`lib.buf_head 取用应报一条 static 跨模块错误:${JSON.stringify(mainDiags)}`
);
assert(
staticDiags[0].message.includes('lib.buf_head'),
`诊断文案应含 lib.buf_head:${JSON.stringify(staticDiags)}`
);
} finally {
program.dispose();
serviceProvider.dispose();
require('fs').rmSync(testRoot, { recursive: true, force: true });
}
});
test('注解内 static 仍受软关键字边界约束:static 单独作为注解名不变', () => {
const code = `static: int = 0
static = 1
`;
const { program, configOptions, uris, testRoot, serviceProvider } = buildProgram({ 'main.vp': code });
try {
const diagnostics = getDiagnostics(program, configOptions, uris['main.vp']);
console.log('=== 注解内 static 边界诊断 ===');
diagnostics.forEach((d) => console.log(` L${d.line} [category=${d.category}] ${d.message}`));
assert.strictEqual(
diagnostics.filter((d) => d.category === 0).length,
0,
`static 作为普通标识符/变量名不应报语法错误:${JSON.stringify(diagnostics)}`
);
} finally {
program.dispose();
serviceProvider.dispose();
require('fs').rmSync(testRoot, { recursive: true, force: true });
}
});
@@ -128,6 +128,69 @@ test('viperlib.vp 中 *args 透传 vsnprintf 不报 tuple 类型错误', () => {
serviceProvider.dispose();
});
/*
* ViperOS VKernel/Kernel/drivers/serial/uart/serial.vp 场景:
* export def printf(fmt: const str, *args) -> int:
* ...
* n: int = viperlib.vsnprintf(&buf, 1024, fmt, args)
* 该文件**没有** `import t`,此前 getViperCPtrType 只认源码可见的 `t.CPtr`,
* 取不到就退回 Python 语义的 tuple → 误报
* 「无法将"tuple"类型的参数分配给函数"vsnprintf"中类型为"str"的参数"ap"」。
* 修法:回退 builtins stub 的 `rawptr`(= t.CPtr,与 `T*` 注解同机制)。
*/
test('未 import t 的文件里 *args 仍是 va_list 指针(serial.vp 场景)', () => {
const fs = require('fs');
const testRoot = path.join(__dirname, '__viper_variadic_noimport__');
fs.rmSync(testRoot, { recursive: true, force: true });
fs.mkdirSync(testRoot, { recursive: true });
const filePath = path.join(testRoot, 'main.vp');
fs.writeFileSync(
filePath,
`import viperlib
export def printf(fmt: const str, *args) -> int:
if fmt is None:
return 0
buf: [1024]char
n: int = viperlib.vsnprintf(&buf, 1024, fmt, args)
return n
`,
'utf8'
);
const { program, serviceProvider, uri, configOptions } = buildProgram(filePath, testRoot, [
TRANSPYC_INCLUDES,
]);
try {
const diagnostics = getDiagnostics(program, configOptions, uri);
console.log('=== *args 无 import t 诊断 ===');
diagnostics.forEach((d) => console.log(` ${d.range} [category=${d.category}] ${d.message}`));
const variadicErrors = diagnostics.filter(
(d) =>
d.message.includes('tuple') ||
d.message.includes('参数 "ap"') ||
d.message.includes('is not defined')
);
assert.strictEqual(
variadicErrors.length,
0,
`*args 应绑定为 va_list 指针(rawptr),不应报 tuple/未定义:${JSON.stringify(variadicErrors)}`
);
assert.strictEqual(
diagnostics.filter((d) => d.category === 0).length,
0,
`不应有任何 error:${JSON.stringify(diagnostics)}`
);
} finally {
program.dispose();
serviceProvider.dispose();
fs.rmSync(testRoot, { recursive: true, force: true });
}
});
test('memhub.vp 中 viperio.Buf.__sizeof__() 不报 Argument missing for parameter "self"', () => {
const { program, serviceProvider, uri, configOptions } = buildProgram(MEMHUB_PATH);
+3 -1
View File
@@ -29,11 +29,13 @@
- Lowercase **`callable[[A, B], R]`** is now supported in a type annotation and means exactly `t.Callable[[A, B], R]` — all three spellings (`callable[...]`, `Callable[...]`, `t.Callable[...]`) and all the usual argument forms (`callable[[int, str], bool]`, `callable[..., bool]`, `callable[[], bool]`, as a parameter, return or variable annotation) evaluate to the same callable type. The parser rewrites the bare name `callable` to `Callable` **only in type-annotation position**, so the built-in function is untouched: `callable(x)` / `if callable(x):` still behave as before. `Callable` is declared in the built-in `builtins.vpi` stub (`Callable: t.CTypedef = t.Callable`, next to `rawptr` / `CConst`), so no `import t` is required. Previously `callable[[int], bool]` silently degraded to the type of the `callable` *function* (the Viper-dialect rule that ignores explicit type arguments on a callable), and bare `Callable` was reported as undefined
- `__attribute__` no longer requires the file to `import t` / `import c`: the `t` / `c` module roots that `__attribute__(...)` desugars into are synthesized as **implicit module names** (zero-length, marked on the AST node — the compiler's attribute handling is AST-level and never checks symbol visibility either). When such a synthetic name cannot be resolved through the lexical scope chain, it falls back to the same-named binding in the built-in `builtins.vpi` stub; the fallback only applies to the desugared nodes, so a hand-written bare `t` / `c` in a file without the import still reports "未定义"
- **Analysis progress in the status bar**: while the language server builds the dependency graph and type-checks the workspace, the bottom-left status bar now shows `$(sync~spin) C:\xxx\1.vp (4/90) 4.44% loading...` — the file currently being analyzed, how many files have been processed out of how many, and the percentage. The worker reports per-file progress (throttled to one message every 50 ms) through a new `analysisProgress` background message and a new `viper/analysisStatus` notification; the client shows a left-aligned `StatusBarItem` and hides it as soon as analysis finishes (or when there is nothing to analyze). Previously the only feedback was the work-done progress bar for open-files analysis, which said nothing about which file was being processed, so a long full-workspace pass was indistinguishable from a hung server
- The **`static` modifier** is now supported (LSP side only; TransPyC/TransPyV are unchanged). It is a *soft* keyword: `static def f(...)` / `static fn f(...)` marks a function (recorded on `FunctionNode.d.viperModifier`, exactly like `virtual`) and `static x: T = v` marks a variable (the annotation is rewritten to `t.CStatic | T`, the same tree TransPyC's `_parse_type_ann` builds by inserting `t.CStatic` at the left of the union, so `static SECRET: int` renders as `static int`). Anywhere else `static` stays an ordinary identifier — `static = 1`, `static: int`, `static(x)`, `def f(static: int)` and `self.static` all parse as before. Its meaning is C's **internal linkage**: a static symbol **cannot be used across modules**. Referencing one from another module is reported as `'lib.helper' 是 static 符号,禁止跨模块使用(static 仅限定义模块内可见)` for both `mod.name` / `mod.fn()` (checked in the module member-access path) and `from mod import name` (checked on the import statement), while same-module references are allowed — the decision is made per declaration file, so a self-import is not flagged either. `static` is also offered by completion and highlighted as a modifier (`storage.modifier.viper`, matched only in front of `def`-family or `name :`)
- The **`static` modifier** is now supported (LSP side only; TransPyC/TransPyV are unchanged). It is a *soft* keyword: `static def f(...)` / `static fn f(...)` marks a function (recorded on `FunctionNode.d.viperModifier`, exactly like `virtual`) and `static x: T = v` marks a variable (the annotation is rewritten to `t.CStatic | T`, the same tree TransPyC's `_parse_type_ann` builds by inserting `t.CStatic` at the left of the union, so `static SECRET: int` renders as `static int`). Anywhere else `static` stays an ordinary identifier — `static = 1`, `static: int`, `static(x)`, `def f(static: int)` and `self.static` all parse as before. Its meaning is C's **internal linkage**: a static symbol **cannot be used across modules**. Referencing one from another module is reported as `'lib.helper' 是 static 符号,禁止跨模块使用(static 仅限定义模块内可见)` for both `mod.name` / `mod.fn()` (checked in the module member-access path) and `from mod import name` (checked on the import statement), while same-module references are allowed — the decision is made per declaration file, so a self-import is not flagged either. `static` is also offered by completion and highlighted as a modifier (`storage.modifier.viper`, matched in front of a `def`-family keyword or a type atom)
- The `f` prefix of an f-string is now highlighted as `storage.type.string.viper` (blue in the default dark theme) instead of being left unstyled. The rule matches the `r` / `u` / `b` / `f` prefix combination immediately preceding a quote, so it covers `f"..."`, `f'''...'''` and mixed spellings like `rf"..."`; a bare `r`/`f`/`b` identifier not followed by a quote is unaffected, and the string body keeps the usual `string.quoted.*` scope
- The **size of a fixed-length array type may be any integer constant expression** now: `part_types: [MAX_VOLUMES * MAX_PARTITIONS]uint8`, `part_lit: [MAX_VOLUMES * 2]uint8`, `part_left: [MAX_VOLUMES - 1]uint8` and `part_shift: [1 << 3]uint8` all parse and check cleanly. Two bugs made this fail before: the parser parsed the `[SIZE]` part while still flagged as a type annotation, so `_parseArithmeticTerm` deliberately stopped at the `*` (it is the C pointer suffix marker there) — the bracket was reported as "“[" 未关闭" and the leftover `* MAX_PARTITIONS]uint8` as "类型表达式中不允许使用解包运算符"; and the type evaluator then evaluated that arithmetic with `EvalFlags.TypeExpression`, reporting "类型表达式中不允许使用二元运算符". The size is now parsed as the value expression it is (the flag is cleared for its duration), and the evaluator allows arithmetic/bitwise operators in a type expression when **both operands are Viper integers** (the case the enclosing comment already described, e.g. `t.CArray[T, MAX_ORDER + 1]`); non-integer operands (`C1 + C2`) are still reported
- **`const` pointers are read-only for dereference assignment**: `lfn: const str` (`CConst | str`, i.e. `const char *`) now allows dereferencing — `*lfn` and `lfn[0]` evaluate to `char` — but writing through the dereference is reported as "不能给 const 指针的解引用赋值("const str" 指向的数据为只读)". This covers **both** spellings, `*lfn = char(65)` (dereference) and `lfn[0] = char(65)` (**subscript assignment**), which share the same diagnostic. Previously the dereference itself was rejected with a bogus "类型"const str"不支持运算符"*"" (the `CConst` modifier in the union was mistaken for a non-pointer operand), and there was no read-only check at all. A non-`const` pointer (`buf: str`) still accepts `*buf = char(65)` and `buf[0] = char(65)`
- C type descriptors are **skipped** when checking iterability, so a `const` pointer can be iterated: `for ch in path:` with `path: const str` (and `const t.CPtr`) no longer reports "“CConst”不可迭代 / 未定义“__iter__”方法". The predicate now recognizes every Viper pointer (CPtr, `str`, `bytes`) rather than only `CPtr`, which is why `const str` had been missed — `CConst` fell through to the "not iterable" path
- The `static` modifier is also accepted **inside a type annotation**: `buf_head: static int = 0` — the spelling actually used throughout ViperOS (`VKernel/Kernel/drivers/**`). It desugars to the very same `t.CStatic | T` and marks the declaration as a static symbol, so it renders as `static int` and the cross-module rule applies identically; previously the `static` inside the annotation was read as an ordinary name and reported `"static" is not defined`. `static` stays a **soft** keyword in this position too — it is consumed only when a type atom follows, so `static` as a variable name (`static: int = 0`, `static = 1`) is untouched. It is now highlighted as a modifier inside annotations as well
- `*args` is bound to the `va_list` **pointer** even when the file doesn't `import t`: the Viper var-arg rule used to look up `t.CPtr` through the source-visible `t` module only, so in a file like ViperOS `drivers/serial/uart/serial.vp` (no `import t`) it fell back to Python's `tuple` semantics and `viperlib.vsnprintf(&buf, 1024, fmt, args)` was reported as "无法将"tuple"类型的参数分配给函数"vsnprintf"中类型为"str"的参数"ap"". The lookup now falls back to the compiler built-in `rawptr` from `builtins.vpi` (= `t.CPtr`), the same mechanism `T*` annotations and `[SIZE]T` already use
## 1.1.425
@@ -55,7 +55,7 @@
},
{
"name": "storage.modifier.viper",
"match": "\\bstatic\\b(?=\\s+(?:def|fn|fun|func|function)\\b|\\s+[A-Za-z_][A-Za-z0-9_]*\\s*:)"
"match": "\\bstatic\\b(?=\\s+(?:def|fn|fun|func|function)\\b|\\s*[A-Za-z_\\[])"
},
{
"name": "storage.modifier.viper",