From 0bc7a7c4d4ff852344b40ac95b52588ef9f0f047 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sun, 27 Sep 2026 18:13:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(vipeye-internal):=20=E6=94=AF=E6=8C=81=20e?= =?UTF-8?q?xport/extern=20=E5=8F=98=E9=87=8F=E5=A3=B0=E6=98=8E=EF=BC=88ext?= =?UTF-8?q?ern=20=E7=A6=81=E6=AD=A2=E5=88=9D=E5=A7=8B=E5=80=BC=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - export NAME: T [= v] -> T | t.CExport(裸名定义,不加模块 SHA1 头),允许初始值; extern NAME: T -> T | t.State(= CExtern | CExport 仅声明),不允许初始值: extern a: int = 1 报「extern 变量声明不能带初始值(符号定义在外部)」 - 与函数形态由第三个 token 区分(`:` 变量 / `(` `[` 函数),与 TPC _is_export_extern_var 同判据;export def / export f() 形态逐字不变 - 存储类标记按修饰符处理:Struct|CPtr 折叠与联合下标 skip 列表改用 isCModifierType(避免 extern p: t.CPtr 折成 CPtr[State]、str|State 下标误报); CArray 元素访问快路径放宽到"联合里的 CArray 成员"(gdt[i].access 仍解析) - typePrinter 增加 CExport->export / State->extern 前缀渲染(hover 显示 export int) - 新增 viperExportExternVar.test.ts 4 用例,jest 809 -> 813 --- .gitignore | 1 + .../src/analyzer/typeEvaluator.ts | 42 ++- .../src/analyzer/typePrinter.ts | 11 +- .../vipeye-internal/src/analyzer/types.ts | 8 +- .../src/localization/localize.ts | 2 + .../src/localization/package.nls.en-us.json | 1 + .../src/localization/package.nls.zh-cn.json | 1 + packages/vipeye-internal/src/parser/parser.ts | 80 +++++- .../src/tests/viperExportExternVar.test.ts | 252 ++++++++++++++++++ packages/vscode-vipeye/CHANGELOG.md | 3 +- 10 files changed, 367 insertions(+), 34 deletions(-) create mode 100644 packages/vipeye-internal/src/tests/viperExportExternVar.test.ts diff --git a/.gitignore b/.gitignore index de5408f0..d1f9ff1d 100644 --- a/.gitignore +++ b/.gitignore @@ -156,6 +156,7 @@ 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__/ packages/vipeye-internal/src/tests/__viper_carray_element__/ +packages/vipeye-internal/src/tests/__viper_export_extern__/ # perfCompare.py cached commit bundles /build/perfCompare/ diff --git a/packages/vipeye-internal/src/analyzer/typeEvaluator.ts b/packages/vipeye-internal/src/analyzer/typeEvaluator.ts index 605292a6..c0850e4f 100644 --- a/packages/vipeye-internal/src/analyzer/typeEvaluator.ts +++ b/packages/vipeye-internal/src/analyzer/typeEvaluator.ts @@ -8263,8 +8263,11 @@ export function createTypeEvaluator( // Viper 扩展:TypeSet 类型组合的下标访问语义。 // Viper 中 `|` 表示类型组合(如 CConst | str = const char*), - // 对 TypeSet 中的修饰类型(CConst/CStatic/CPtr/CVoid/CDefine/CArray/CAttr)跳过下标访问, + // 对 TypeSet 中的修饰类型(isCModifierType:CConst/CStatic/CVolatile/ + // CExport/State/CExtern/CInline/…/CAttr)与 CPtr/CVoid 跳过下标访问, // 让 combineTypes 只合并非修饰类型的结果(如 str/bytes 的下标返回 int/CChar)。 + // 注:CArray 不在此列 —— 它有真实的元素访问语义,由本函数开头的 + // CArray 快路径统一处理(裸类型与联合成员都取元素类型)。 const viperIndexFileInfo = AnalyzerNodeInfo.getFileInfo(node); const isViperTypeSetIndex = IsViperDialect(viperIndexFileInfo.executionEnvironment.dialect) && @@ -8272,14 +8275,7 @@ export function createTypeEvaluator( baseTypeResult.type.priv.subtypes.some( (subtype) => isClassInstance(subtype) && - (isCConstType(subtype) || - isCStaticType(subtype) || - isCVolatileType(subtype) || - isCPtrType(subtype) || - isCVoidType(subtype) || - isCDefineType(subtype) || - isViperAttrMarkerType(subtype) || - isCArrayType(subtype)) + (isCModifierType(subtype) || isCPtrType(subtype) || isCVoidType(subtype)) ); // Viper 扩展:PEP 695 风格显式类型实参 `fn[TypeArgs]`。 @@ -8301,17 +8297,13 @@ export function createTypeEvaluator( const selfType = isTypeVar(unexpandedSubtype) ? unexpandedSubtype : undefined; // Viper TypeSet:修饰类型跳过下标访问,避免产生 Unknown 污染结果。 + // (CArray 除外:它有元素访问语义,走本函数开头的 CArray 快路径。) if ( isViperTypeSetIndex && isClassInstance(concreteSubtype) && - (isCConstType(concreteSubtype) || - isCStaticType(concreteSubtype) || - isCVolatileType(concreteSubtype) || + (isCModifierType(concreteSubtype) || isCPtrType(concreteSubtype) || - isCVoidType(concreteSubtype) || - isCDefineType(concreteSubtype) || - isViperAttrMarkerType(concreteSubtype) || - isCArrayType(concreteSubtype)) + isCVoidType(concreteSubtype)) ) { return undefined; } @@ -8866,16 +8858,22 @@ export function createTypeEvaluator( // 下标恒为 Unknown,进而 `buf[i].field` 误报「无法解析类型上的成员访问」。 // 按 C 语义直接取第一个类型实参(元素类型),与 CPtr 分支读 // `baseType.priv.typeArgs[0]` 同法,不依赖 TypeVar 替换。 + // + // 基类型也可以是**联合**(`CArray[T, N] | t.CExport` / `| t.State` —— + // 存储类标记只作修饰,见 parser._parseViperExportExternVarStatement): + // 此时取联合里的 CArray 成员,避免修饰符成员被当"不可下标"报错。 const viperArrayFileInfo = AnalyzerNodeInfo.getFileInfo(node); if ( IsViperDialect(viperArrayFileInfo.executionEnvironment.dialect) && - usage.method === 'get' && - isClassInstance(baseType) && - isCArrayType(baseType) && - baseType.priv.typeArgs && - baseType.priv.typeArgs.length > 0 + usage.method !== 'del' ) { - return { type: makeTopLevelTypeVarsConcrete(baseType.priv.typeArgs[0]) }; + const arrayType = isClassInstance(baseType) && isCArrayType(baseType) + ? baseType + : findSubtype(baseType, (subtype) => isClassInstance(subtype) && isCArrayType(subtype)); + const arrayTypeArgs = (arrayType as ClassType | undefined)?.priv.typeArgs; + if (arrayTypeArgs && arrayTypeArgs.length > 0) { + return { type: makeTopLevelTypeVarsConcrete(arrayTypeArgs[0]) }; + } } // Viper 扩展:CPtr[T] 指针下标解引用语义。 diff --git a/packages/vipeye-internal/src/analyzer/typePrinter.ts b/packages/vipeye-internal/src/analyzer/typePrinter.ts index b3c8e7ef..7256965c 100644 --- a/packages/vipeye-internal/src/analyzer/typePrinter.ts +++ b/packages/vipeye-internal/src/analyzer/typePrinter.ts @@ -726,8 +726,11 @@ function printTypeInternal( // Viper 用联合(TypeSet)表示「类型 + 描述符」,如 `CConst | CPtr[int]`(const int*)、 // `CVolatile | int`(volatile int)。展示时须把它们当作**前缀限定符**, // 而不是联合成员,否则会暴露出内部表示("CConst | rawptr[int]")。 -// 仅覆盖有唯一 C 关键字的修饰符;State / CExport / CPass / CDefine -// (声明标记、编译期元指令)另有语义,保持原样。 +// `CExport`(`export x: T`)/ `State`(`extern x: T` = CExtern | CExport)是本 +// 语言软关键字 `export` / `extern` 声明变量的脱糖产物(与 TPC +// `_parse_export_extern_var` 同形),同属存储类修饰,故与 static/register 等一样 +// 按前缀渲染(`export int` / `extern int`)。 +// 其余声明标记 / 编译期元指令(CPass / CDefine)保持原样。 function getViperModifierKeyword(type: Type): string | undefined { if (!isClass(type)) { return undefined; @@ -748,6 +751,10 @@ function getViperModifierKeyword(type: Type): string | undefined { return 'inline'; case 'CExtern': return 'extern'; + case 'CExport': + return 'export'; + case 'State': + return 'extern'; default: return undefined; } diff --git a/packages/vipeye-internal/src/analyzer/types.ts b/packages/vipeye-internal/src/analyzer/types.ts index b6590c68..c497779c 100644 --- a/packages/vipeye-internal/src/analyzer/types.ts +++ b/packages/vipeye-internal/src/analyzer/types.ts @@ -4223,12 +4223,12 @@ export function combineTypes(subtypes: Type[], options?: CombineTypesOptions): T isClass(subtype) && // None 是空指针取值而非结构体,不能作为 CPtr 的类型参数(否则 None | CPtr[T] 会误折叠成 CPtr[None]) !ClassType.isBuiltIn(subtype, 'NoneType') && + // 修饰符(CConst/CStatic/CVolatile/CExport/State/CExtern/CInline/CDefine/CAttr 等) + // 只作修饰,不能当被指向类型:`t.CPtr | t.State`(extern 变量的存储类标记)保持联合, + // 否则会折叠成 CPtr[State] 这种把修饰符当结构体的错误类型。 + !isCModifierType(subtype) && !isCVoidType(subtype) && !isCArrayType(subtype) && - !isCConstType(subtype) && - !isCStaticType(subtype) && - !isCVolatileType(subtype) && - !isCDefineType(subtype) && !ClassType.isBuiltIn(subtype, 'object') ) { structSubtypes.push(subtype); diff --git a/packages/vipeye-internal/src/localization/localize.ts b/packages/vipeye-internal/src/localization/localize.ts index f5cb1758..93279672 100644 --- a/packages/vipeye-internal/src/localization/localize.ts +++ b/packages/vipeye-internal/src/localization/localize.ts @@ -1212,6 +1212,8 @@ export namespace Localizer { new ParameterizedString<{ name: string }>(getRawString('Diagnostic.viperStaticUsedOutsideOfModule')); export const viperConstDerefAssignment = () => new ParameterizedString<{ type: string }>(getRawString('Diagnostic.viperConstDerefAssignment')); + export const viperExternVarCannotHaveValue = () => + getRawString('Diagnostic.viperExternVarCannotHaveValue'); export const viperLenNoLenMethod = () => new ParameterizedString<{ name: string }>(getRawString('Diagnostic.viperLenNoLenMethod')); export const unhashableDictKey = () => getRawString('Diagnostic.unhashableDictKey'); diff --git a/packages/vipeye-internal/src/localization/package.nls.en-us.json b/packages/vipeye-internal/src/localization/package.nls.en-us.json index a41b4d87..10ccf347 100644 --- a/packages/vipeye-internal/src/localization/package.nls.en-us.json +++ b/packages/vipeye-internal/src/localization/package.nls.en-us.json @@ -1639,6 +1639,7 @@ "viperExternBodyInvalid": "The extern function \"{name}\" must have a body of only 'pass' or 'None'", "viperStaticUsedOutsideOfModule": "\"{name}\" is a static symbol and cannot be used across modules (static is visible only inside its defining module)", "viperConstDerefAssignment": "Cannot assign through a dereference of a const pointer (the data pointed to by \"{type}\" is read-only)", + "viperExternVarCannotHaveValue": "An extern variable declaration cannot have an initializer (the symbol is defined externally)", "viperLenNoLenMethod": "Type \"{name}\" has no __len__ method and cannot be used with len()", "unhashableDictKey": "Dictionary key must be hashable", "unhashableSetEntry": { diff --git a/packages/vipeye-internal/src/localization/package.nls.zh-cn.json b/packages/vipeye-internal/src/localization/package.nls.zh-cn.json index 38292352..ac70df2f 100644 --- a/packages/vipeye-internal/src/localization/package.nls.zh-cn.json +++ b/packages/vipeye-internal/src/localization/package.nls.zh-cn.json @@ -631,6 +631,7 @@ "viperExternBodyInvalid": "extern 函数 \"{name}\" 的函数体必须为 pass 或 None", "viperStaticUsedOutsideOfModule": "\"{name}\" 是 static 符号,禁止跨模块使用(static 仅限定义模块内可见)", "viperConstDerefAssignment": "不能给 const 指针的解引用赋值(\"{type}\" 指向的数据为只读)", + "viperExternVarCannotHaveValue": "extern 变量声明不能带初始值(符号定义在外部)", "viperLenNoLenMethod": "类型 \"{name}\" 没有 __len__ 方法,不能用于 len()", "unhashableDictKey": "字典密钥必须可哈希", "unhashableSetEntry": "Set 条目必须是可哈希的", diff --git a/packages/vipeye-internal/src/parser/parser.ts b/packages/vipeye-internal/src/parser/parser.ts index 0e5d84f4..62b5c0c0 100644 --- a/packages/vipeye-internal/src/parser/parser.ts +++ b/packages/vipeye-internal/src/parser/parser.ts @@ -530,6 +530,13 @@ export class Parser { (modifierText === 'extern' || modifierText === 'export') && this._peekToken(1).type === TokenType.Identifier ) { + // Viper 扩展:`export NAME: T [= v]` / `extern NAME: T` 变量声明。 + // 与 `extern A(...)` / `export A(...)` 函数声明由第三个 token 区分: + // `:` 是变量(类型注解),`(` / `[` 是函数(参数表 / 泛型)。 + // (与 TPC includes/ast/parser.vp `_is_export_extern_var` 同判据) + if (this._peekToken(2).type === TokenType.Colon) { + return this._parseViperExportExternVarStatement(modifierToken, modifierText); + } // extern/export 直接声明形式:`extern A(...)`。 // 消费修饰关键字后,下一个标识符即函数名;defToken 用修饰关键字 // 作为范围起点(diagnostic 定位)。 @@ -5434,7 +5441,7 @@ export class Parser { this._viperStaticDescriptorInAnnotation = true; return BinaryOperationNode.create( - this._createViperStaticMarker(staticToken.start), + this._createViperStorageClassMarker(staticToken.start, 'CStatic'), innerType, this._createViperSyntheticBarToken(staticToken), OperatorType.BitwiseOr @@ -5575,11 +5582,13 @@ export class Parser { return nameNode; } - // Viper 扩展:合成 `t.CStatic` 属性标记(模块根是隐式名,不要求源码 import t)。 - private _createViperStaticMarker(start: number): ExpressionNode { + // Viper 扩展:合成 `t.` 存储类标记(模块根是隐式名,不要求源码 + // import t)。`CStatic`(static 变量)、`CExport`(export 变量)、 + // `State`(extern 变量 = CExtern | CExport)共用本函数。 + private _createViperStorageClassMarker(start: number, memberName: string): ExpressionNode { return MemberAccessNode.create( this._createViperImplicitModuleName(start, 't'), - NameNode.create(IdentifierToken.create(start, 0, 'CStatic', /* comments */ undefined)) + NameNode.create(IdentifierToken.create(start, 0, memberName, /* comments */ undefined)) ); } @@ -5626,7 +5635,7 @@ export class Parser { // 故锚点取内层注解自身的起点,零长度合成名正好落在注解内侧。 const anchorStart = annotationNode.d.annotation.start; const wrappedAnnotation = BinaryOperationNode.create( - this._createViperStaticMarker(anchorStart), + this._createViperStorageClassMarker(anchorStart, 'CStatic'), annotationNode.d.annotation, OperatorToken.create(anchorStart, 0, OperatorType.BitwiseOr, /* comments */ undefined), OperatorType.BitwiseOr @@ -5637,6 +5646,67 @@ export class Parser { return statementList; } + // Viper 扩展:`export NAME: T [= v]` / `extern NAME: T` 模块级变量声明。 + // 与 TPC includes/ast/parser.vp `_parse_export_extern_var` 逐条对应: + // export → 注解改写为 `T | t.CExport`(定义端裸名,**不加模块 SHA1 头** + // —— 内联汇编 / C 符号对接必需),允许带初始值; + // extern → 注解改写为 `T | t.State`(= t.CExtern | t.CExport:仅声明 + 裸名, + // 与 `extern def` 同口径),**不允许初始值**(符号定义在外部), + // `extern a: int = 1` 报错(响亮失败,非静默)。 + // `t` 用隐式模块根(不要求源码 import t)。 + // 存储类标记只作修饰:TypeSet 语义下不改变数据类型(isCModifierType 统一跳过), + // 故 `gdt[i].field` / `gdt = ...` 等访问与普通变量一致。 + private _parseViperExportExternVarStatement(modifierToken: KeywordToken, modifierText: string): StatementNode { + this._getNextToken(); // 消费 export / extern + const statementList = this._parseSimpleStatement(); + const isExtern = modifierText === 'extern'; + + let annotationNode: TypeAnnotationNode | undefined; + let hasInitialValue = false; + for (const inner of statementList.d.statements) { + if (inner.nodeType === ParseNodeType.TypeAnnotation) { + annotationNode = inner; + break; + } + if (inner.nodeType === ParseNodeType.Assignment) { + hasInitialValue = true; + const leftExpr = inner.d.leftExpr; + if (leftExpr.nodeType === ParseNodeType.TypeAnnotation) { + annotationNode = leftExpr; + break; + } + } + } + + if (!annotationNode) { + // 调用方已保证 `export`/`extern` 后是 `名字 :`;走到这里说明写法异常。 + this._addSyntaxError(LocMessage.viperModifierRequiresDef().format({ name: modifierText }), modifierToken); + return statementList; + } + + if (isExtern && hasInitialValue) { + this._addSyntaxError(LocMessage.viperExternVarCannotHaveValue(), modifierToken); + } + + // 合成的存储类标记贴在注解**右侧**(`T | t.CExport` / `T | t.State`, + // 与 TPC `BinOp(ann, BitOr, storage_attr)` 同形)。锚点取内层注解的**末尾**: + // 标记零长度、落在注解范围内侧,既不会让子节点跑到父节点范围之外 + // (绑定期 testWalker 的 "Child node ... is not contained within its parent"), + // 也保持「子节点按位置递增」。 + const annotationEnd = annotationNode.d.annotation.start + annotationNode.d.annotation.length; + const annotation = annotationNode.d.annotation; + const markerAnnotation = BinaryOperationNode.create( + annotation, + this._createViperStorageClassMarker(annotationEnd, isExtern ? 'State' : 'CExport'), + OperatorToken.create(annotationEnd, 0, OperatorType.BitwiseOr, /* comments */ undefined), + OperatorType.BitwiseOr + ); + markerAnnotation.parent = annotationNode; + annotationNode.d.annotation = markerAnnotation; + + return statementList; + } + // Viper 扩展:把 `@__attribute__(...)` 装饰器脱糖为 // `@c.Attribute(t.attr.a, t.attr.b)`——与 TPC/TPV 的 // includes/ast/parser.vp `_DesugarMagicAttribute` 逐字段同形,因此 class 与 def diff --git a/packages/vipeye-internal/src/tests/viperExportExternVar.test.ts b/packages/vipeye-internal/src/tests/viperExportExternVar.test.ts new file mode 100644 index 00000000..b4d032df --- /dev/null +++ b/packages/vipeye-internal/src/tests/viperExportExternVar.test.ts @@ -0,0 +1,252 @@ +/* + * viperExportExternVar.test.ts + * + * 验证 Viper 的 `export` / `extern` **变量**声明(仅 LSP 侧实现,TPC/TPV 不动): + * ① `export NAME: T [= v]` —— 注解脱糖为 `T | t.CExport`(与 TPC + * includes/ast/parser.vp `_parse_export_extern_var` 同形:定义端**裸名**, + * 不加模块 SHA1 头),**允许**初始值; + * ② `extern NAME: T` —— 注解脱糖为 `T | t.State`(= t.CExtern | t.CExport: + * 仅声明 + 裸名),**不允许**初始值:`extern a: int = 1` 报错; + * ③ 与函数形态(`export def f()` / `export f(...)`)由第三个 token 区分: + * `:` 是变量,`(` / `[` 是函数;函数形态逐字不变。 + * ④ 存储类标记只作修饰:`export gdt: [7]gdt_entry = []` 的 `gdt[i].field` + * 仍能解析出元素类型(`[]` 零初始化不报类型错误), + * 展示为前缀限定符(`export CArray[gdt_entry, 7]` / `extern int`)。 + * + * 运行:npx jest src/tests/viperExportExternVar.test.ts --runInBand + */ + +import assert from 'assert'; +import * as path from 'path'; + +import { ImportResolver } from '../analyzer/importResolver'; +import { ParseTreeWalker } from '../analyzer/parseTreeWalker'; +import { Program } from '../analyzer/program'; +import { ConfigOptions, ExecutionEnvironment } from '../common/configOptions'; +import { NullConsole } from '../common/console'; +import { RealTempFile, createFromRealFileSystem } from '../common/realFileSystem'; +import { createServiceProvider } from '../common/serviceProviderExtensions'; +import { UriEx } from '../common/uri/uriUtils'; +import { FullAccessHost } from '../common/fullAccessHost'; +import { IndexNode, NameNode, ParseNodeType, TypeAnnotationNode } from '../parser/parseNodes'; + +(global as any).__rootDirectory = path.resolve(); + +function buildProgram(code: string) { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const fs = require('fs'); + const testRoot = path.join(__dirname, '__viper_export_extern__'); + fs.rmSync(testRoot, { recursive: true, force: true }); + fs.mkdirSync(testRoot, { recursive: true }); + const filePath = path.join(testRoot, 'main.vp'); + fs.writeFileSync(filePath, code, 'utf8'); + + const projectRoot = UriEx.file(testRoot); + const configOptions = new ConfigOptions(projectRoot); + configOptions.internalTestMode = true; + configOptions.initializeTypeCheckingMode('standard'); + + const execEnv = new ExecutionEnvironment( + 'python', + projectRoot, + configOptions.diagnosticRuleSet, + configOptions.defaultPythonVersion, + configOptions.defaultPythonPlatform, + [projectRoot], + /* skipNativeLibraries */ false, + /* defaultDialect */ 'viper' + ); + configOptions.executionEnvironments = [execEnv]; + + const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); + const program = new Program(importResolver, configOptions, serviceProvider); + + const uri = UriEx.file(filePath); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + return { program, configOptions, uri, serviceProvider, testRoot }; +} + +function getDiagnostics(program: Program, configOptions: ConfigOptions, uri: any) { + const sourceFile = program.getSourceFile(uri); + assert(sourceFile, 'source file not found'); + return (sourceFile.getDiagnostics(configOptions) || []).map((diag: any) => ({ + category: diag.category, + line: diag.range ? diag.range.start.line + 1 : -1, + message: diag.message as string, + })); +} + +/** 按名字收集模块级变量的展示类型(printType 走 typePrinter,与 hover 一致)。 */ +function collectVarTypes(program: Program, uri: any): Map { + const sourceFile = program.getSourceFile(uri); + const parseResults = program.getParseResults(uri); + assert(parseResults, 'parse results not found'); + const evaluator = program.evaluator!; + const result = new Map(); + + class Collector extends ParseTreeWalker { + override visitName(node: NameNode): boolean { + const parent = node.parent; + if (parent?.nodeType === ParseNodeType.TypeAnnotation && (parent as TypeAnnotationNode).d.valueExpr === node) { + const type = evaluator.getType(node); + if (type && !result.has(node.d.value)) { + result.set(node.d.value, program.printType(type)); + } + } + return true; + } + override visitIndex(node: IndexNode): boolean { + const type = evaluator.getType(node); + const text = sourceFile!.getParseResults()!.text.substr(node.start, node.length); + if (text.startsWith('gdt[') || text.startsWith('ext_str[')) { + result.set(text, type ? program.printType(type) : '(undefined)'); + } + return true; + } + } + new Collector().walk(parseResults.parserOutput.parseTree); + return result; +} + +test('export 变量声明:允许初始值,元素访问/展示均正常', () => { + const code = `import t + +class gdt_entry: + limit_low: uint16 + base_low: uint16 + base_mid: uint8 + access: uint8 + flags: uint8 + base_high: uint8 + +export gdt: [7]gdt_entry = [] +export tss_buf: [104]uint8 +export counter: int = 3 + +def touch() -> uint8: + gdt[1].access = 0x92 + return gdt[0].access +`; + const { program, configOptions, uri, serviceProvider, testRoot } = buildProgram(code); + + try { + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== export 变量诊断 ==='); + diagnostics.forEach((d) => console.log(` L${d.line} [category=${d.category}] ${d.message}`)); + + const real = diagnostics.filter((d) => d.category === 0 || d.category === 1); + assert.strictEqual(real.length, 0, `export 变量声明不应报错:${JSON.stringify(real)}`); + + const types = collectVarTypes(program, uri); + console.log('=== 变量类型 ==='); + types.forEach((v, k) => console.log(` ${k}: ${v}`)); + + // 存储类标记按前缀限定符展示(与 static int 同一机制) + assert.strictEqual(types.get('counter'), 'export int', `counter 应展示为 export int`); + assert( + types.get('gdt') === 'export CArray[gdt_entry, 7]' || + types.get('gdt')?.startsWith('export CArray[gdt_entry'), + `gdt 应展示为 export CArray[gdt_entry, 7],实际: ${types.get('gdt')}` + ); + // 元素访问解析出元素类型(CArray 快路径,联合形态也生效) + assert.strictEqual(types.get('gdt[0]'), 'gdt_entry', `gdt[0] 应为 gdt_entry`); + } finally { + program.dispose(); + serviceProvider.dispose(); + require('fs').rmSync(testRoot, { recursive: true, force: true }); + } +}); + +test('extern 变量声明:不带初始值合法,展示为 extern 前缀', () => { + const code = `import t + +extern ext_flag: int +extern ext_str: str +extern ext_ptr: t.CPtr + +def touch() -> char: + return ext_str[0] +`; + const { program, configOptions, uri, serviceProvider, testRoot } = buildProgram(code); + + try { + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== extern 变量诊断 ==='); + diagnostics.forEach((d) => console.log(` L${d.line} [category=${d.category}] ${d.message}`)); + + const real = diagnostics.filter((d) => d.category === 0 || d.category === 1); + assert.strictEqual(real.length, 0, `extern 变量声明(不带初始值)不应报错:${JSON.stringify(real)}`); + + const types = collectVarTypes(program, uri); + console.log('=== 变量类型 ==='); + types.forEach((v, k) => console.log(` ${k}: ${v}`)); + + assert.strictEqual(types.get('ext_flag'), 'extern int', `ext_flag 应展示为 extern int`); + assert.strictEqual(types.get('ext_str'), 'extern str', `ext_str 应展示为 extern str`); + // `extern ext_ptr: t.CPtr`:CPtr 是裸指针模板,不能被修饰符折叠成 CPtr[State] + assert( + types.get('ext_ptr')?.startsWith('extern rawptr'), + `ext_ptr 应展示为 extern rawptr[...],实际: ${types.get('ext_ptr')}` + ); + assert.strictEqual(types.get('ext_str[0]'), 'char', `ext_str[0] 应为 char`); + } finally { + program.dispose(); + serviceProvider.dispose(); + require('fs').rmSync(testRoot, { recursive: true, force: true }); + } +}); + +test('extern 变量声明带初始值报错(符号定义在外部)', () => { + const { program, configOptions, uri, serviceProvider, testRoot } = buildProgram('extern a: int = 1\n'); + + try { + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== extern 带初始值诊断 ==='); + diagnostics.forEach((d) => console.log(` L${d.line} [category=${d.category}] ${d.message}`)); + + const target = diagnostics.filter((d) => d.message.includes('extern 变量声明不能带初始值')); + assert.strictEqual( + target.length, + 1, + `extern a: int = 1 应报 1 条「不能带初始值」:${JSON.stringify(diagnostics)}` + ); + assert.strictEqual(target[0].category, 0, `应为 error:${JSON.stringify(target[0])}`); + assert.strictEqual(target[0].line, 1, `报错应落在第 1 行:${JSON.stringify(target[0])}`); + } finally { + program.dispose(); + serviceProvider.dispose(); + require('fs').rmSync(testRoot, { recursive: true, force: true }); + } +}); + +test('export/extern 函数形态不受影响(第三个 token 为 ( / [)', () => { + const code = `export def published(x: int) -> int: + return x + +extern def external_fn(x: int) -> int + +export direct_decl(x: int) -> int + +def touch() -> int: + return published(1) + external_fn(2) + direct_decl(3) +`; + const { program, configOptions, uri, serviceProvider, testRoot } = buildProgram(code); + + try { + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== export/extern 函数形态诊断 ==='); + diagnostics.forEach((d) => console.log(` L${d.line} [category=${d.category}] ${d.message}`)); + + const real = diagnostics.filter((d) => d.category === 0 || d.category === 1); + assert.strictEqual(real.length, 0, `export/extern 函数声明不应报错:${JSON.stringify(real)}`); + } finally { + program.dispose(); + serviceProvider.dispose(); + require('fs').rmSync(testRoot, { recursive: true, force: true }); + } +}); diff --git a/packages/vscode-vipeye/CHANGELOG.md b/packages/vscode-vipeye/CHANGELOG.md index 12c0a718..1d070392 100644 --- a/packages/vscode-vipeye/CHANGELOG.md +++ b/packages/vscode-vipeye/CHANGELOG.md @@ -36,7 +36,8 @@ - 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 -- Indexing a C array now yields the **element type**, so member access on an element resolves: with `kbd_buf: [KBD_BUF_SIZE]usb_kbd_event`, `kbd_buf[i].modifier` used to report "无法解析类型上的成员"modifier"访问,对象可能未定义" and the member name got no semantic token at all (plain white). The CArray stub declares `class CArray(CType, Generic[T, N])` with `def __getitem__(self, index: int) -> T`, but Viper-side `TypeVar(...)` does not produce substitutable type variables (the class's type parameters end up as synthesized `__P0` / `__P1`), so the return type never became concrete. The Viper index path now reads the **first type argument** directly (C semantics: `arr[i]` has the element type), the same way the `CPtr[T]` branch reads `priv.typeArgs[0]`; this covers both `[SIZE]T` and the explicit `t.CArray[T, N]` spelling +- Indexing a C array now yields the **element type**, so member access on an element resolves: with `kbd_buf: [KBD_BUF_SIZE]usb_kbd_event`, `kbd_buf[i].modifier` used to report "无法解析类型上的成员"modifier"访问,对象可能未定义" and the member name got no semantic token at all (plain white). The CArray stub declares `class CArray(CType, Generic[T, N])` with `def __getitem__(self, index: int) -> T`, but Viper-side `TypeVar(...)` does not produce substitutable type variables (the class's type parameters end up as synthesized `__P0` / `__P1`), so the return type never became concrete. The Viper index path now reads the **first type argument** directly (C semantics: `arr[i]` has the element type), the same way the `CPtr[T]` branch reads `priv.typeArgs[0]`; this covers both `[SIZE]T` and the explicit `t.CArray[T, N]` spelling, and works for a union base too (`CArray[T, N] | t.CExport`) +- **`export` / `extern` on variable declarations** are now supported: `export gdt: [7]gdt_entry = []`, `export tss_buf: [104]uint8` and `extern ext_flag: int`. The variable form is told apart from the function form (`export def f()` / `export f(...)`) by the third token — `:` is a variable (type annotation), `(` / `[` is a function, the same criterion as TransPyC's `_is_export_extern_var`). They desugar exactly like TransPyC's `_parse_export_extern_var`: `export NAME: T [= v]` becomes `T | t.CExport` (bare-named definition, **no module SHA1 header**), `extern NAME: T` becomes `T | t.State` (= `CExtern | CExport`, declaration only). An **initializer is allowed for `export` but rejected for `extern`** — `extern a: int = 1` reports "extern 变量声明不能带初始值(符号定义在外部)", matching the compiler's fail-fast. Storage-class markers render as a prefix qualifier (`export CArray[gdt_entry, 7]`, `extern int`), and the marker is treated as a modifier everywhere else: in the `Struct | CPtr` fold (so `extern p: t.CPtr` stays a pointer instead of degrading to `CPtr[State]`), in the union index access, and as a CArray element access (so `gdt[i].access` still resolves to `uint8` and the zero-initializer `= []` is accepted) ## 1.1.425