diff --git a/1.vp b/1.vp index d9f882d1..740755b4 100644 --- a/1.vp +++ b/1.vp @@ -1,10 +1,74 @@ +import w32.win32console as w32cmd import stdio import t, c -def add(a: int, b: int) -> int: - return a + b +pagecode: t.CDefine = 65001 -def main() -> int: - stdio.printf("Hello, World!") - return 0 \ No newline at end of file + +class TestStruct(): + a: t.CInt + b: t.CInt + + +class Obj(): + val: t.CInt + + +export def main() -> int: + w32cmd.SetConsoleOutputCP(pagecode) + w32cmd.SetConsoleCP(pagecode) + print("=== Unary * / & (Deref/AddrOf) Test ===") + + # --- Test 1: 标量 & 取地址 + * 解引用 --- + x: int = 42 + px: int | t.CPtr = &x + d: int = *px + print(f"T1: x={x} *(&x)={d} (expect 42, 42)") + if d == 42: + print("T1 OK") + else: + print("T1 FAIL") + + # --- Test 2: 通过指针写回(c.DerefAs 兼容形式) --- + c.DerefAs(px, 100) + print(f"T2: after c.DerefAs(px, 100) x={x} (expect 100)") + if x == 100: + print("T2 OK") + else: + print("T2 FAIL") + + # --- Test 3: 数组元素地址 &arr[i] + * 解引用 --- + a: t.CArray[t.CInt, 4] + a[0] = 7 + pa: t.CInt | t.CPtr = &a[0] + v3: int = *pa + print(f"T3: a[0]={a[0]} *&a[0]={v3} (expect 7, 7)") + if v3 == 7: + print("T3 OK") + else: + print("T3 FAIL") + + # --- Test 4: 结构体字段地址 &s.f + * 解引用 --- + s: TestStruct + s.a = 10 + pf: t.CInt | t.CPtr = &s.a + v4: int = *pf + print(f"T4: s.a={s.a} *&s.a={v4} (expect 10, 10)") + if v4 == 10: + print("T4 OK") + else: + print("T4 FAIL") + + # --- Test 5: &obj 对象指针(对象变量即指针) --- + o: Obj + o.val = 99 + po: Obj | t.CPtr = &o + print(f"T5: o.val={o.val} &o->val={po.val} (expect 99, 99)") + if po.val == 99: + print("T5 OK") + else: + print("T5 FAIL") + + print("=== Unary * / & Test Done ===") + return 0 diff --git a/packages/vipeye-internal/src/analyzer/binder.ts b/packages/vipeye-internal/src/analyzer/binder.ts index adfd89da..9692fce4 100644 --- a/packages/vipeye-internal/src/analyzer/binder.ts +++ b/packages/vipeye-internal/src/analyzer/binder.ts @@ -432,10 +432,25 @@ export class Binder extends ParseTreeWalker { // 仅当导入的最终目标是目录(而非具体文件)时才标黄。 const lastResolvedUri = importResult.resolvedUris[importResult.resolvedUris.length - 1]; const isFinalTargetDirectory = !lastResolvedUri || lastResolvedUri.isEmpty(); + // Viper 扩展:`from . import X` 的请求符号(X)能在目录中经隐式导入解析 + //(存在同名 X.vp 文件)时,说明目录作为包已可正常使用,不再报"命名空间包不完整"。 + const importFromNode = + node.parent?.nodeType === ParseNodeType.ImportFrom ? (node.parent as ImportFromNode) : undefined; + const allSymbolsResolvable = + importFromNode !== undefined && + importFromNode.d.imports.length > 0 && + importFromNode.d.imports.every((importSymbolNode) => { + const importedName = importSymbolNode.d.name.d.value; + return ( + importResult.implicitImports?.has(importedName) || + importResult.filteredImplicitImports?.has(importedName) + ); + }); if ( importResult.isImportFound && importResult.isNamespacePackage && isFinalTargetDirectory && + !allSymbolsResolvable && IsViperDialect(this._fileInfo.executionEnvironment.dialect) ) { this._fileInfo.diagnosticSink.addDiagnosticWithTextRange( @@ -1319,7 +1334,11 @@ export class Binder extends ParseTreeWalker { const postForLabel = this._createBranchLabel(); // Determine if this loop is guaranteed to execute at least once - const isGuaranteedToExecute = this._isNonEmptyListOrTupleLiteral(node.d.iterableExpr); + // Viper 方言(C 风格):for 循环变量在循环后仍视为已定义(保留迭代类型), + // 不产生 Unbound 分支——与 Python 语义(循环可能零次执行 → Unbound)不同。 + const isGuaranteedToExecute = IsViperDialect(this._fileInfo.executionEnvironment.dialect) + ? true + : this._isNonEmptyListOrTupleLiteral(node.d.iterableExpr); this._addAntecedent(preForLabel, this._currentFlowNode!); this._currentFlowNode = preForLabel; diff --git a/packages/vipeye-internal/src/analyzer/checker.ts b/packages/vipeye-internal/src/analyzer/checker.ts index 6ebf26f0..91c270f8 100644 --- a/packages/vipeye-internal/src/analyzer/checker.ts +++ b/packages/vipeye-internal/src/analyzer/checker.ts @@ -238,6 +238,13 @@ function isViperExternAllowedBody(stmt: ParseNode): boolean { return false; } +function isViperNoneLiteral(expr: ParseNode | undefined): boolean { + if (!expr || expr.nodeType !== ParseNodeType.Constant) { + return false; + } + return (expr as ConstantNode).d.constType === KeywordType.None; +} + export class Checker extends ParseTreeWalker { private readonly _moduleNode: ModuleNode; private readonly _fileInfo: AnalyzerFileInfo; @@ -1085,6 +1092,24 @@ export class Checker extends ParseTreeWalker { returnTypeResult.expectedTypeDiagAddendum?.getEffectiveTextRange() ); } + + // Viper 扩展:返回类型为 None(无返回值)的函数,return 只能写成 + // 裸 return 或 return None 字面量。若 return 的是另一个返回 None + // 的表达式(如 `return g()`,g 的调用结果在 C 语义中无值),必须报错。 + if ( + returnTypeMatches && + node.d.expr && + IsViperDialect(this._fileInfo.executionEnvironment.dialect) && + isNoneInstance(adjReturnType) && + isNoneInstance(returnType) && + !isViperNoneLiteral(node.d.expr) + ) { + this._evaluator.addDiagnostic( + DiagnosticRule.reportReturnType, + LocMessage.viperReturnExprInNoneFunction(), + node.d.expr + ); + } } } diff --git a/packages/vipeye-internal/src/analyzer/codeFlowEngine.ts b/packages/vipeye-internal/src/analyzer/codeFlowEngine.ts index de3bf877..faf5c5f6 100644 --- a/packages/vipeye-internal/src/analyzer/codeFlowEngine.ts +++ b/packages/vipeye-internal/src/analyzer/codeFlowEngine.ts @@ -13,6 +13,7 @@ import { ConsoleInterface } from '../common/console'; import { assert, fail } from '../common/debug'; +import { IsViperDialect } from '../common/dialect'; import { convertOffsetToPosition } from '../common/positionUtils'; import { ArgCategory, ExpressionNode, ParseNode, ParseNodeType } from '../parser/parseNodes'; import { getFileInfo, getImportInfo } from './analyzerNodeInfo'; @@ -50,6 +51,7 @@ import { FunctionType, isClass, isClassInstance, + isCTypeSubclassType, isFunction, isFunctionOrOverloaded, isInstantiableClass, @@ -74,6 +76,7 @@ import { derivesFromStdlibClass, doForEachSubtype, isIncompleteUnknown, + isNoneInstance, isTypeAliasPlaceholder, mapSubtypes, } from './typeUtils'; @@ -1729,6 +1732,7 @@ export function getCodeFlowEngine( function isCallNoReturn(evaluator: TypeEvaluator, flowNode: FlowCall) { const node = flowNode.node; const fileInfo = getFileInfo(node); + const isViperDialect = IsViperDialect(fileInfo.executionEnvironment?.dialect); // Assume that calls within a pyi file are not "NoReturn" calls. if (fileInfo.isStubFile) { @@ -1803,7 +1807,7 @@ export function getCodeFlowEngine( const isCallAwaited = node.parent?.nodeType === ParseNodeType.Await; if (isFunction(callSubtype)) { - if (isFunctionNoReturn(callSubtype, isCallAwaited)) { + if (isFunctionNoReturn(evaluator, callSubtype, isCallAwaited, isViperDialect)) { noReturnTypeCount++; } } else if (isOverloaded(callSubtype)) { @@ -1813,7 +1817,7 @@ export function getCodeFlowEngine( OverloadedType.getOverloads(callSubtype).forEach((overload) => { overloadCount++; - if (isFunctionNoReturn(overload, isCallAwaited)) { + if (isFunctionNoReturn(evaluator, overload, isCallAwaited, isViperDialect)) { noReturnOverloadCount++; } }); @@ -1859,9 +1863,45 @@ export function getCodeFlowEngine( } } - function isFunctionNoReturn(functionType: FunctionType, isCallAwaited: boolean) { + function isFunctionNoReturn( + evaluator: TypeEvaluator, + functionType: FunctionType, + isCallAwaited: boolean, + isViperDialect: boolean + ) { const returnType = FunctionType.getEffectiveReturnType(functionType, /* includeInferred */ false); if (returnType) { + if (isViperDialect) { + // Viper 方言:函数声明返回 t.State(C 风格 noreturn 标记,仅声明不定义, + // 如 sys.exit / stdlib.exit 的 `-> t.State`)视为 NoReturn,调用后流程终止。 + let hasStateReturn = false; + doForEachSubtype(returnType, (retSubtype) => { + if ( + isClass(retSubtype) && + isCTypeSubclassType(retSubtype) && + retSubtype.shared.name === 'State' + ) { + hasStateReturn = true; + } + }); + if (hasStateReturn) { + return true; + } + + // Viper 无返回注解的函数被隐式声明为 `-> None`(C 语义 void), + // 掩盖了实际不返回的 fail-fast 函数(如函数体以 sys.exit(1) 结尾)。 + // 惰性推断(inferredReturnType)可能尚未就绪,直接通过 flow 分析 + // 函数体判定是否从不返回(isAfterNodeReachable 主动触发 flow)。 + if (isNoneInstance(returnType)) { + const inferred = functionType.shared.inferredReturnType?.type; + if (inferred && isNever(inferred)) { + return true; + } + if (!isAfterNodeReachable(evaluator, functionType)) { + return true; + } + } + } if ( isClassInstance(returnType) && ClassType.isBuiltIn(returnType, ['Coroutine', 'CoroutineType']) && diff --git a/packages/vipeye-internal/src/analyzer/constructors.ts b/packages/vipeye-internal/src/analyzer/constructors.ts index 0b2ddf65..47a81f23 100644 --- a/packages/vipeye-internal/src/analyzer/constructors.ts +++ b/packages/vipeye-internal/src/analyzer/constructors.ts @@ -258,32 +258,62 @@ function validateNewAndInitMethods( const overloadsUsedForCall: FunctionType[] = []; let newMethodReturnType: Type | undefined; - // Validate __new__ if it is present. + // Viper 扩展:`__new__` 作为分配器时(返回裸指针或实例|指针联合,如 + // stmts.vp 中 FunctionDef 的 `__new__` 返回 `FunctionDef | t.CPtr`),其参数 + // 不参与构造调用校验——Viper 中 `__new__` 由编译器按分配逻辑调用,构造调用 + // 的用户参数只与 `__init__` 签名匹配(否则 7 参 `__init__` 配 6 参 `__new__` + // 会误报 "Expected 6 positional arguments")。此时仅取 `__new__` 的返回类型 + // 用于后续 `__init__` 绑定判断。 + let isViperNewAllocator = false; if (newMethodTypeResult) { - // Use speculative mode for arg expressions because we don't know whether - // we'll need to re-evaluate these expressions later for __init__. - const newCallResult = validateNewMethod( - evaluator, - errorNode, - argList, - type, - skipUnknownArgCheck, - inferenceContext, - newMethodTypeResult, - /* useSpeculativeModeForArgs */ true - ); + if (isFunctionOrOverloaded(newMethodTypeResult.type)) { + doForEachSignature(newMethodTypeResult.type, (signature) => { + const allocNewReturnType = FunctionType.getEffectiveReturnType(signature); + if (allocNewReturnType) { + doForEachSubtype(allocNewReturnType, (subtype) => { + if (isCPtrType(subtype)) { + isViperNewAllocator = true; + } + }); + } + }); + } - if (newCallResult.argumentErrors) { - argumentErrors = true; + if (isViperNewAllocator) { + // 跳过 __new__ 参数校验,直接取其返回类型。 + if (isFunctionOrOverloaded(newMethodTypeResult.type)) { + doForEachSignature(newMethodTypeResult.type, (signature) => { + if (!newMethodReturnType) { + newMethodReturnType = FunctionType.getEffectiveReturnType(signature); + } + }); + } } else { - appendArray(overloadsUsedForCall, newCallResult.overloadsUsedForCall ?? []); - } + // Use speculative mode for arg expressions because we don't know whether + // we'll need to re-evaluate these expressions later for __init__. + const newCallResult = validateNewMethod( + evaluator, + errorNode, + argList, + type, + skipUnknownArgCheck, + inferenceContext, + newMethodTypeResult, + /* useSpeculativeModeForArgs */ true + ); - if (newCallResult.isTypeIncomplete) { - isTypeIncomplete = true; - } + if (newCallResult.argumentErrors) { + argumentErrors = true; + } else { + appendArray(overloadsUsedForCall, newCallResult.overloadsUsedForCall ?? []); + } - newMethodReturnType = newCallResult.returnType; + if (newCallResult.isTypeIncomplete) { + isTypeIncomplete = true; + } + + newMethodReturnType = newCallResult.returnType; + } } if ( @@ -325,16 +355,33 @@ function validateNewAndInitMethods( } // Validate __init__ if it's present. + // + // Viper 扩展:`__new__` 返回联合类型(如 `FunctionDef | t.CPtr`,实例或裸 + // 指针)时,`isClassInstance` 对整个联合返回 false,导致 `__init__` 校验被 + // 跳过,构造调用只按 `__new__` 签名校验(误报:FunctionDef 的 6 参 `__new__` + // 报 "Expected 6 positional arguments",而 `__init__` 实际有 7 参含 + // type_params)。Viper 中 `__new__` 只负责分配内存,`__init__` 仍会被调用, + // 因此联合返回只要含 CPtr(Viper 分配语义)就继续校验 `__init__`。 + let newMethodReturnTypeIsInstance = isClassInstance(newMethodReturnType); + if (!newMethodReturnTypeIsInstance) { + doForEachSubtype(newMethodReturnType, (subtype) => { + if (isCPtrType(subtype)) { + newMethodReturnTypeIsInstance = true; + } + }); + } + if ( !isNever(newMethodReturnType) && !shouldSkipInitEvaluation(evaluator, type, newMethodReturnType) && - isClassInstance(newMethodReturnType) + newMethodReturnTypeIsInstance ) { // If the __new__ method returned the same type as the class it's constructing // but didn't supply solved type arguments, we'll ignore its specialized return // type and rely on the __init__ method to supply the type arguments instead. let initMethodBindToType = newMethodReturnType; if ( + isClassInstance(initMethodBindToType) && initMethodBindToType.priv.typeArgs && initMethodBindToType.priv.typeArgs.some((typeArg) => isUnknown(typeArg)) ) { @@ -354,6 +401,12 @@ function validateNewAndInitMethods( initMethodBindToType = ClassType.cloneAsInstance(type); } + // 纯类实例时保持原类型,联合/CPtr 时已替换为类实例; + // 保证传给 getBoundInitMethod / validateInitMethod 的一定是 ClassType。 + if (!isClassInstance(initMethodBindToType)) { + initMethodBindToType = ClassType.cloneAsInstance(type); + } + // Determine whether the class overrides the object.__init__ method. const initMethodDiag = new DiagnosticAddendum(); initMethodTypeResult = getBoundInitMethod(evaluator, errorNode, initMethodBindToType, initMethodDiag); @@ -389,7 +442,7 @@ function validateNewAndInitMethods( } } - if (!validatedArgExpressions && newMethodTypeResult) { + if (!validatedArgExpressions && newMethodTypeResult && !isViperNewAllocator) { // If we skipped the __init__ method and the __new__ method was evaluated only // speculatively, evaluate it non-speculatively now so we can report errors. if (!evaluator.isSpeculativeModeInUse(errorNode)) { @@ -409,6 +462,13 @@ function validateNewAndInitMethods( returnType = newMethodReturnType; } + // Viper 扩展:分配器 `__new__` 且无 `__init__` 时,参数不做构造校验, + // 返回类型沿用 `__new__` 的返回类型(实例|指针联合或裸指针)。 + if (!validatedArgExpressions && isViperNewAllocator) { + validatedArgExpressions = true; + returnType = newMethodReturnType; + } + // If the class doesn't override object.__new__ or object.__init__, use the // fallback constructor type evaluation for the `object` class. if (!newMethodTypeResult && !initMethodTypeResult) { diff --git a/packages/vipeye-internal/src/analyzer/importResolver.ts b/packages/vipeye-internal/src/analyzer/importResolver.ts index d801f83a..9c511f48 100644 --- a/packages/vipeye-internal/src/analyzer/importResolver.ts +++ b/packages/vipeye-internal/src/analyzer/importResolver.ts @@ -82,10 +82,11 @@ export function createImportedModuleDescriptor(moduleName: string): ImportedModu type CachedImportResults = Map; const supportedNativeLibExtensions = ['.pyd', '.so', '.dylib']; -// Viper 模式下同时支持 .vp/.vpi(主)和 .py/.pyi(兼容回退)。 +// Viper 模式下同时支持 .vp/.vpi/.vpnci(主)和 .py/.pyi(兼容回退)。 +// .vpi 为可编译注解,.vpnci 为不可编译注解(配置与 .vpi 一致)。 // .py/.pyi 支持用于 TransPyC 源码(TPC/TPV 编译器源码仍是 .py 扩展名), // 以及用户未迁移到 .vp 的旧代码。 -export const supportedSourceFileExtensions = ['.vp', '.vpi', '.py', '.pyi']; +export const supportedSourceFileExtensions = ['.vp', '.vpi', '.vpnci', '.py', '.pyi']; export const supportedFileExtensions = [...supportedSourceFileExtensions, ...supportedNativeLibExtensions]; // Should we allow partial resolution for third-party packages? Some use tricks @@ -223,7 +224,11 @@ export class ImportResolver { result.nonStubImportResult.resolvedUris.length - 1 ]; - if (nonEmptyUri.hasExtension('.vp') || nonEmptyUri.hasExtension('.vpi')) { + if ( + nonEmptyUri.hasExtension('.vp') || + nonEmptyUri.hasExtension('.vpi') || + nonEmptyUri.hasExtension('.vpnci') + ) { // We allow pyi in case there are multiple pyi for a compiled module such as // numpy.random.mtrand sourceFileUris.push(nonEmptyUri); @@ -821,7 +826,13 @@ export class ImportResolver { let strippedFileName: string; let isNativeLib = false; - if (fileExt === '.vp' || fileExt === '.vpi' || fileExt === '.py' || fileExt === '.pyi') { + if ( + fileExt === '.vp' || + fileExt === '.vpi' || + fileExt === '.vpnci' || + fileExt === '.py' || + fileExt === '.pyi' + ) { strippedFileName = stripFileExtension(filePath.fileName); } else if ( _isNativeModuleFileExtension(fileExt) && @@ -1402,6 +1413,8 @@ export class ImportResolver { // Viper 模式下同时支持 .vp/.vpi(主)和 .py/.pyi(兼容回退)。 const pyFilePath = dirPath.initPyUri; const pyiFilePath = dirPath.initPyiUri; + // .vpnci 不可编译注解(与 .vpi 同级) + const vpnciInitFilePath = dirPath.combinePathsUnsafe('__init__.vpnci'); // .py/.pyi 兼容回退路径 const compatPyFilePath = dirPath.combinePathsUnsafe('__init__.py'); const compatPyiFilePath = dirPath.combinePathsUnsafe('__init__.pyi'); @@ -1414,6 +1427,13 @@ export class ImportResolver { isStubFile = true; } isInitFilePresent = true; + } else if (allowPyi && this.fileExistsCached(vpnciInitFilePath)) { + importLogger?.log(`Resolved import with file '${vpnciInitFilePath}'`); + resolvedPaths.push(vpnciInitFilePath); + if (isLastPart) { + isStubFile = true; + } + isInitFilePresent = true; } else if (this.fileExistsCached(pyFilePath)) { importLogger?.log(`Resolved import with file '${pyFilePath}'`); resolvedPaths.push(pyFilePath); @@ -1440,6 +1460,7 @@ export class ImportResolver { if (dirName) { const sameNameVpPath = dirPath.combinePathsUnsafe(dirName).addExtension('.vp'); const sameNameVpiPath = dirPath.combinePathsUnsafe(dirName).addExtension('.vpi'); + const sameNameVpnciPath = dirPath.combinePathsUnsafe(dirName).addExtension('.vpnci'); if (allowPyi && this.fileExistsCached(sameNameVpiPath)) { importLogger?.log(`Resolved import with Viper same-name file '${sameNameVpiPath}'`); resolvedPaths.push(sameNameVpiPath); @@ -1447,6 +1468,13 @@ export class ImportResolver { isStubFile = true; } isInitFilePresent = true; + } else if (allowPyi && this.fileExistsCached(sameNameVpnciPath)) { + importLogger?.log(`Resolved import with Viper same-name file '${sameNameVpnciPath}'`); + resolvedPaths.push(sameNameVpnciPath); + if (isLastPart) { + isStubFile = true; + } + isInitFilePresent = true; } else if (this.fileExistsCached(sameNameVpPath)) { importLogger?.log(`Resolved import with Viper same-name file '${sameNameVpPath}'`); resolvedPaths.push(sameNameVpPath); @@ -1469,6 +1497,7 @@ export class ImportResolver { implicitImports = this.findImplicitImports(moduleDescriptor.nameParts.join('.'), dirPath, [ pyFilePath, pyiFilePath, + vpnciInitFilePath, ]); break; } @@ -1481,6 +1510,8 @@ export class ImportResolver { // 用于 TransPyC 源码(仍是 .py 扩展名)。 const pyFilePath = dirPath.packageUri; const pyiFilePath = dirPath.packageStubUri; + // .vpnci 不可编译注解(与 .vpi 同级) + const vpnciFilePath = dirPath.addExtension('.vpnci'); // .py/.pyi 兼容回退路径 const compatPyFilePath = dirPath.addExtension('.py'); const compatPyiFilePath = dirPath.addExtension('.pyi'); @@ -1492,6 +1523,12 @@ export class ImportResolver { if (isLastPart) { isStubFile = true; } + } else if (allowPyi && this.fileExistsCached(vpnciFilePath)) { + importLogger?.log(`Resolved import with file '${vpnciFilePath}'`); + resolvedPaths.push(vpnciFilePath); + if (isLastPart) { + isStubFile = true; + } } else if (this.fileExistsCached(pyFilePath)) { importLogger?.log(`Resolved import with file '${pyFilePath}'`); resolvedPaths.push(pyFilePath); @@ -2117,7 +2154,11 @@ export class ImportResolver { if (!entry.name.startsWith('_')) { const packageInit = dirRoot.combinePaths('__init__.vpi'); - if (this._fileSystemCache.existsSync(packageInit)) { + const packageInitVpnci = dirRoot.combinePaths('__init__.vpnci'); + if ( + this._fileSystemCache.existsSync(packageInit) || + this._fileSystemCache.existsSync(packageInitVpnci) + ) { addDirectoryStdlibModule(moduleName); } } diff --git a/packages/vipeye-internal/src/analyzer/operations.ts b/packages/vipeye-internal/src/analyzer/operations.ts index 45a5b2a9..47ab9517 100644 --- a/packages/vipeye-internal/src/analyzer/operations.ts +++ b/packages/vipeye-internal/src/analyzer/operations.ts @@ -30,6 +30,7 @@ import { EvalFlags, MagicMethodDeprecationInfo, TypeEvaluator, TypeResult } from import { InferenceContext, convertToInstantiable, + convertToInstance, getLiteralTypeClassName, getTypeCondition, getUnionSubtypeCount, @@ -50,6 +51,7 @@ import { } from './typeUtils'; import { ClassType, + ModuleType, NeverType, Type, TypeBase, @@ -67,6 +69,7 @@ import { isFunctionOrOverloaded, isViperIntegerType, isInstantiableClass, + isModule, isNever, isUnion, } from './types'; @@ -578,12 +581,11 @@ export function getTypeOfAugmentedAssignment( // Viper 扩展:str/bytes 指针增强赋值(C 指针算术)。 // Viper 中 str/bytes 是 i8* 指针,`dest += 1`(指针递增)是合法的 // C 指针算术,结果类型保持 str/bytes。Python 中 str 不可变故无此语义。 - // 仅当运算符为 + 且右操作数是整数时生效。 + // 仅当运算符为 + 且右操作数是整数(含 C 整数类型)时生效。 if (IsViperDialect(getFileInfo(node).executionEnvironment.dialect)) { const leftIsStrBytes = isClassInstance(leftSubtypeUnexpanded) && ClassType.isBuiltIn(leftSubtypeUnexpanded, ['str', 'bytes']); - const rightIsInt = isClassInstance(rightSubtypeUnexpanded) && - ClassType.isBuiltIn(rightSubtypeUnexpanded, 'int'); + const rightIsInt = isViperIntegerType(rightSubtypeUnexpanded); if (leftIsStrBytes && rightIsInt) { return leftSubtypeUnexpanded; } @@ -689,6 +691,19 @@ export function getTypeOfUnaryOperation( flags: EvalFlags, inferenceContext: InferenceContext | undefined ): TypeResult { + const isViperDialect = IsViperDialect(getFileInfo(node).executionEnvironment.dialect); + + // Viper 扩展:类型注解上下文中的一元 '*' 表示 C 指针类型(T* = CPtr[T])。 + // parser 将 `T*`/`T**`/`T***` 后缀解析为 UnaryOperationNode(Multiply) 嵌套, + // 此处须在 TypeExpression 守卫(unaryOperationNotAllowed)之前处理。 + if ( + isViperDialect && + node.d.operator === OperatorType.Multiply && + (flags & EvalFlags.TypeExpression) !== 0 + ) { + return getTypeOfViperPointerAnnotation(evaluator, node); + } + if ((flags & EvalFlags.TypeExpression) !== 0) { evaluator.addDiagnostic(DiagnosticRule.reportInvalidTypeForm, LocMessage.unaryOperationNotAllowed(), node); return { type: UnknownType.create() }; @@ -703,6 +718,19 @@ export function getTypeOfUnaryOperation( return { type: NeverType.createNever(), isIncomplete }; } + // Viper 扩展:一元 '*'(解引用)与 '&'(取地址),与 C 语义一致。 + // 在 Viper 方言下,前缀 '*'/'&' 被 parser 解析为 UnaryOperation + // (operator = Multiply / BitwiseAnd),此处直接按 C 语义求值, + // 不走魔法方法路径(指针没有 __pos__ 等)。 + if (isViperDialect) { + if (node.d.operator === OperatorType.Multiply) { + return getTypeOfViperDeref(evaluator, node, exprType, isIncomplete); + } + if (node.d.operator === OperatorType.BitwiseAnd) { + return getTypeOfViperAddrOf(evaluator, node, exprType, isIncomplete); + } + } + // Map unary operators to magic functions. Note that the bitwise // invert has two magic functions that are aliases of each other. const unaryOperatorMap: { [operator: number]: string } = { @@ -820,6 +848,159 @@ export function getTypeOfUnaryOperation( return { type, isIncomplete, magicMethodDeprecationInfo: deprecatedInfo }; } +// Viper 扩展:一元 '*'(解引用)类型求值,与 C 语义一致。 +// - CPtr[X](含 `X | t.CPtr` 化简形式)→ 返回目标类型 X(类对象转为实例) +// - Unknown/Any → 透传 +// - 非指针类型 → 报错(C 语义:invalid type argument of unary '*') +function getTypeOfViperDeref( + evaluator: TypeEvaluator, + node: UnaryOperationNode, + exprType: Type, + isIncomplete: boolean | undefined +): TypeResult { + const resultTypes: Type[] = []; + let sawNonPointer = false; + + mapSubtypes(exprType, (subtype) => { + if (isAnyOrUnknown(subtype)) { + resultTypes.push(subtype); + return subtype; + } + + if (isClassInstance(subtype) && isCPtrType(subtype)) { + const typeArgs = subtype.priv.typeArgs; + if (typeArgs && typeArgs.length > 0) { + const pointee = typeArgs[0]; + resultTypes.push( + isClass(pointee) && !isClassInstance(pointee) ? convertToInstance(pointee) : pointee + ); + } else { + // 裸 CPtr(无类型参数)无法确定目标类型。 + resultTypes.push(UnknownType.create()); + } + return subtype; + } + + // 非指针类型:C 语义报错。 + sawNonPointer = true; + return subtype; + }); + + if (sawNonPointer) { + evaluator.addDiagnostic( + DiagnosticRule.reportOperatorIssue, + LocMessage.typeNotSupportUnaryOperator().format({ + operator: printOperator(node.d.operator), + type: evaluator.printType(exprType), + }), + node + ); + } + + if (resultTypes.length === 0) { + return { type: UnknownType.create(isIncomplete), isIncomplete }; + } + + return { type: combineTypes(resultTypes), isIncomplete }; +} + +// Viper 扩展:类型注解上下文的一元 '*' → C 指针类型 CPtr[T](即 `T*` 后缀语法)。 +// 与 `T | t.CPtr` 联合化简(combineTypes → CPtr[T])的表示一致: +// 类对象(instantiable)先转为实例再作为 CPtr 的类型参数;TypeVar 直接作为类型参数 +// (泛型中的 `T*` → CPtr[T])。每个嵌套层递归求值,`T**` 自然得到 CPtr[CPtr[T]]。 +function getTypeOfViperPointerAnnotation(evaluator: TypeEvaluator, node: UnaryOperationNode): TypeResult { + const innerResult = evaluator.getTypeOfExpression(node.d.expr, EvalFlags.TypeExpression); + const innerType = innerResult.type; + const resultTypes: Type[] = []; + let sawInvalidType = false; + + mapSubtypes(innerType, (subtype) => { + if (isAnyOrUnknown(subtype)) { + resultTypes.push(subtype); + return subtype; + } + + const typeArg = isClass(subtype) && !isClassInstance(subtype) ? convertToInstance(subtype) : subtype; + const cptrType = getViperCPtrTypeForTypeArg(evaluator, node, typeArg); + if (cptrType) { + resultTypes.push(cptrType); + return subtype; + } + + sawInvalidType = true; + return subtype; + }); + + if (sawInvalidType) { + evaluator.addDiagnostic( + DiagnosticRule.reportInvalidTypeForm, + LocMessage.typeNotSupportUnaryOperator().format({ + operator: printOperator(node.d.operator), + type: evaluator.printType(innerType), + }), + node + ); + } + + if (resultTypes.length === 0) { + return { type: UnknownType.create(innerResult.isIncomplete), isIncomplete: innerResult.isIncomplete }; + } + + return { type: combineTypes(resultTypes), isIncomplete: innerResult.isIncomplete }; +} + +// Viper 扩展:一元 '&'(取地址)类型求值,与 C 语义一致。 +// - x 类型为 X → 返回 CPtr[X](即 `X | t.CPtr` 的 Viper 指针表示) +function getTypeOfViperAddrOf( + evaluator: TypeEvaluator, + node: UnaryOperationNode, + exprType: Type, + isIncomplete: boolean | undefined +): TypeResult { + const cptrType = getViperCPtrTypeForTypeArg(evaluator, node, exprType); + if (!cptrType) { + return { type: UnknownType.create(isIncomplete), isIncomplete }; + } + return { type: cptrType, isIncomplete }; +} + +// 构造 CPtr[typeArg](Viper 指针类型),通过 t 模块的 CPtr 泛型类特化。 +function getViperCPtrTypeForTypeArg( + evaluator: TypeEvaluator, + node: UnaryOperationNode, + typeArg: Type +): Type | undefined { + const tSymbolWithScope = evaluator.lookUpSymbolRecursive(node, 't', /* honorCodeFlow */ true); + if (!tSymbolWithScope) { + return undefined; + } + + const tEffectiveType = evaluator.getEffectiveTypeOfSymbolForUsage( + tSymbolWithScope.symbol, + /* usageNode */ undefined, + /* useLastDecl */ true + ).type; + if (!isModule(tEffectiveType)) { + return undefined; + } + + const cptrSymbol = ModuleType.getField(tEffectiveType, 'CPtr'); + if (!cptrSymbol) { + return undefined; + } + + const cptrType = evaluator.getEffectiveTypeOfSymbolForUsage( + cptrSymbol, + /* usageNode */ undefined, + /* useLastDecl */ true + ).type; + if (isInstantiableClass(cptrType)) { + return convertToInstance(ClassType.specialize(cptrType, [typeArg])); + } + + return undefined; +} + export function getTypeOfTernaryOperation( evaluator: TypeEvaluator, node: TernaryNode, @@ -1382,9 +1563,26 @@ function validateArithmeticOperation( ClassType.isBuiltIn(leftSubtypeExpanded, ['int', 'float', 'bool', 'bytes', 'str']); const rightIsBuiltinNum = isClassInstance(rightSubtypeExpanded) && ClassType.isBuiltIn(rightSubtypeExpanded, ['int', 'float', 'bool', 'bytes', 'str']); + // str/bytes 指针算术优先:一方是 str/bytes(i8* 指针)、另一方是 + // 整数(含 C 整数类型)且运算符为 + 或 - 时,不走 CType 分支 + // (否则 `str + CInt` 会误返回 CInt),由下方指针算术分支处理。 + // 注意:`int - str` 不是指针算术(指针不能减整数的反向), + // 如 `CChar - '0'` 数字字符转换仍走 CType 分支。 + const leftIsStrBytes = isClassInstance(leftSubtypeExpanded) && + ClassType.isBuiltIn(leftSubtypeExpanded, ['str', 'bytes']); + const rightIsStrBytes = isClassInstance(rightSubtypeExpanded) && + ClassType.isBuiltIn(rightSubtypeExpanded, ['str', 'bytes']); + const isPointerArith = + (operator === OperatorType.Add && + ((leftIsStrBytes && isViperIntegerType(rightSubtypeExpanded)) || + (rightIsStrBytes && isViperIntegerType(leftSubtypeExpanded)))) || + (operator === OperatorType.Subtract && + leftIsStrBytes && + (isViperIntegerType(rightSubtypeExpanded) || rightIsStrBytes)); // 任一方是 CType 子类,另一方是 CType 子类或 Python 内置数值/字符串类型 - if ((leftIsCType && (rightIsCType || rightIsBuiltinNum)) || - (rightIsCType && (leftIsCType || leftIsBuiltinNum))) { + if (!isPointerArith && + ((leftIsCType && (rightIsCType || rightIsBuiltinNum)) || + (rightIsCType && (leftIsCType || leftIsBuiltinNum)))) { // 算术/位运算结果类型:优先返回 CType 子类(保持类型语义) if (leftIsCType) { return leftSubtypeExpanded; @@ -1405,10 +1603,11 @@ function validateArithmeticOperation( ClassType.isBuiltIn(leftSubtypeExpanded, ['str', 'bytes']); const rightIsStrBytes = isClassInstance(rightSubtypeExpanded) && ClassType.isBuiltIn(rightSubtypeExpanded, ['str', 'bytes']); - const leftIsInt = isClassInstance(leftSubtypeExpanded) && - ClassType.isBuiltIn(leftSubtypeExpanded, 'int'); - const rightIsInt = isClassInstance(rightSubtypeExpanded) && - ClassType.isBuiltIn(rightSubtypeExpanded, 'int'); + // 整数操作数须涵盖 Viper C 整数类型(CInt 等),否则 + // `str + CInt`(如 resp_buf + total)会落入下方 CType 算术分支 + // 误返回整数类型,而指针偏移应保持 str/bytes(i8*)指针类型。 + const leftIsInt = isViperIntegerType(leftSubtypeExpanded); + const rightIsInt = isViperIntegerType(rightSubtypeExpanded); if (operator === OperatorType.Add) { if ((leftIsStrBytes && rightIsInt) || (leftIsInt && rightIsStrBytes)) { // 返回指针类型(str/bytes) diff --git a/packages/vipeye-internal/src/analyzer/packageTypeVerifier.ts b/packages/vipeye-internal/src/analyzer/packageTypeVerifier.ts index 9d2ebe21..daf86865 100644 --- a/packages/vipeye-internal/src/analyzer/packageTypeVerifier.ts +++ b/packages/vipeye-internal/src/analyzer/packageTypeVerifier.ts @@ -462,7 +462,11 @@ export class PackageTypeVerifier { if (isFile) { const fileExtension = getFileExtension(entry.name); - if (fileExtension === '.vp' || fileExtension === '.vpi') { + if ( + fileExtension === '.vp' || + fileExtension === '.vpi' || + fileExtension === '.vpnci' + ) { const nameWithoutExtension = stripFileExtension(entry.name); if (nameWithoutExtension === '__init__') { diff --git a/packages/vipeye-internal/src/analyzer/patternMatching.ts b/packages/vipeye-internal/src/analyzer/patternMatching.ts index a89b832f..507414fb 100644 --- a/packages/vipeye-internal/src/analyzer/patternMatching.ts +++ b/packages/vipeye-internal/src/analyzer/patternMatching.ts @@ -12,6 +12,7 @@ import { appendArray } from '../common/collectionUtils'; import { assert } from '../common/debug'; import { DiagnosticAddendum } from '../common/diagnostic'; +import { IsViperDialect } from '../common/dialect'; import { DiagnosticRule } from '../common/diagnosticRules'; import { LocAddendum, LocMessage } from '../localization/localize'; import { @@ -29,6 +30,7 @@ import { PatternValueNode, } from '../parser/parseNodes'; import { CodeFlowReferenceExpressionNode } from './codeFlowTypes'; +import { getFileInfo } from './analyzerNodeInfo'; import { addConstraintsForExpectedType } from './constraintSolver'; import { ConstraintTracker } from './constraintTracker'; import { DeclarationType } from './declaration'; @@ -833,6 +835,7 @@ function narrowTypeBasedOnClassPattern( pattern: PatternClassNode, isPositiveTest: boolean ): Type { + const isViperDialect = IsViperDialect(getFileInfo(pattern).executionEnvironment?.dialect); let exprType = evaluator.getTypeOfExpression(pattern.d.className, EvalFlags.CallBaseDefaults).type; // If this is a class (but not a type alias that refers to a class), @@ -1058,6 +1061,33 @@ function narrowTypeBasedOnClassPattern( } if (isClassInstance(subjectSubtypeExpanded)) { + // Viper 扩展:指针 subject 的变体模式收窄。 + // `match x: case LLVMType.Struct(...)` 中 subject 为 CPtr[LLVMType](指针)时, + // 收窄结果应是 CPtr[LLVMType.Struct](指向变体的指针),而非普通变体值 Struct。 + // 否则 case 体内继续把 x 传给 CPtr 参数(如 build_alloca(builder, x))会误报 + // "Struct 不可分配给 CPtr[LLVMType]"。 + // 判定用 fullName 前缀(LLVMType.Struct ⊂ LLVMType):REnum 变体类与 + // 基类之间 assignType 不一定成立(变体不直接继承基类),故不能用 assignType。 + if ( + isViperDialect && + isCPtrType(subjectSubtypeExpanded) && + isInstantiableClass(expandedSubtype) + ) { + const subjectTypeArgs = subjectSubtypeExpanded.priv.typeArgs; + const subjectPointee = + subjectTypeArgs && subjectTypeArgs.length > 0 ? subjectTypeArgs[0] : undefined; + if ( + subjectPointee && + isClassInstance(subjectPointee) && + !isAnyOrUnknown(subjectPointee) && + expandedSubtype.shared.fullName.startsWith(subjectPointee.shared.fullName + '.') + ) { + const patternInstance = ClassType.cloneAsInstance(expandedSubtype); + const cptrResult = ClassType.specialize(subjectSubtypeExpanded, [patternInstance]); + return addConditionToType(cptrResult, getTypeCondition(subjectSubtypeExpanded)); + } + } + let resultType: Type; if (evaluator.assignType(ClassType.cloneAsInstance(expandedSubtype), subjectSubtypeExpanded)) { diff --git a/packages/vipeye-internal/src/analyzer/program.ts b/packages/vipeye-internal/src/analyzer/program.ts index 80c8481d..59262bcf 100644 --- a/packages/vipeye-internal/src/analyzer/program.ts +++ b/packages/vipeye-internal/src/analyzer/program.ts @@ -15,6 +15,7 @@ import { OperationCanceledException, throwIfCancellationRequested } from '../com import { ConfigOptions, ExecutionEnvironment, matchFileSpecs } from '../common/configOptions'; import { ConsoleInterface, StandardConsole } from '../common/console'; import { isThenable } from '../common/core'; +import { appendArray } from '../common/collectionUtils'; import * as debug from '../common/debug'; import { assert } from '../common/debug'; import { Diagnostic, DiagnosticCategory } from '../common/diagnostic'; @@ -30,6 +31,8 @@ import { Range, TextRange, doRangesIntersect } from '../common/textRange'; import { Duration, timingStats } from '../common/timing'; import { Uri } from '../common/uri/uri'; import { ParseFileResults, ParserOutput } from '../parser/parser'; +import { MemberAccessNode, ParseNodeType } from '../parser/parseNodes'; +import { DeclarationType } from './declaration'; import { RequiringAnalysisCount } from './analysis'; import { AbsoluteModuleDescriptor, ImportLookupResult, LookupImportOptions } from './analyzerFileInfo'; import { CellChainIndex, CellChainIndexProvider } from './cellChainIndex'; @@ -157,6 +160,10 @@ export class Program { private _editModeTracker = new EditModeTracker(); private _sourceFileFactory: ISourceFileFactory; + // Viper 扩展:includes/builtins.vp(真实源码,非 .vpi 存根)的 typedef 符号 + // 合并状态。key = builtinsImport.uri.key,避免对每个文件重复扫描/合并。 + private _viperBuiltinsVpMergedKeys = new Set(); + constructor( initialImportResolver: ImportResolver, initialConfigOptions: ConfigOptions, @@ -287,6 +294,9 @@ export class Program { this._configOptions = configOptions; this._importResolver.setConfigOptions(configOptions); + // Viper 扩展:配置变化(includes 等搜索路径调整)后重置 builtins.vp 合并状态。 + this._viperBuiltinsVpMergedKeys.clear(); + // Create a new evaluator with the updated config options. this._createNewEvaluator(); } @@ -1879,6 +1889,13 @@ export class Program { // the builtin module. builtinsScope = getScopeIfAvailable(fileToBind.chainedSourceFile) ?? getScopeIfAvailable(fileToBind.builtinsImport); + + // Viper 扩展:将项目 includes 中 builtins.vp(真实源码,非 .vpi 存根) + // 的 t.CTypedef 类型别名(rowptr/size_t 等)合并进 builtin 作用域。 + // 对齐 TransPyC TypeAnnotationResolver / TPV _lookup_builtins_typedef 的 + // "builtins 最后匹配"语义:builtins 始终纳入编译链但不在用户文件的 + // import 闭包内,其 typedef 声明需作为注解名解析的兜底。 + this._mergeViperBuiltinsVpSymbols(fileToBind, builtinsScope); } if (fileToBind.sourceFile.isParseRequired()) { @@ -1902,6 +1919,125 @@ export class Program { return true; } + // Viper 扩展:将项目 includes 中 builtins.vp(真实源码,非 .vpi 存根)的 + // t.CTypedef 类型别名(rowptr/size_t 等)合并进 builtin 作用域。 + // + // 背景:TransPyC/TPV 的"builtins 最后匹配"——builtins 模块始终纳入编译链, + // 但不在用户文件的 import 闭包内,其 `NAME: t.CTypedef = TYPE` 声明(如 + // builtins.vp 中的 rowptr = t.CPtr、size_t = __stdint.size_t)不随 + // from_imports 注入,编译器按源文件扫描 builtins.vp 作为注解名解析兜底 + // (见 TransPyC lib/core/TypeAnnotationResolver.py)。 + // + // LSP 侧 builtin 作用域仍以 builtins.vpi(typeshed 存根,提供 int/str/Asm 等) + // 为基准,这里仅补充 builtins.vp 中 builtin 作用域缺失的 typedef 符号, + // 不覆盖既有内建语义。 + private _mergeViperBuiltinsVpSymbols(fileToBind: SourceFileInfo, builtinsScope: Scope | undefined) { + if (!builtinsScope || !fileToBind.builtinsImport) { + return; + } + + const key = fileToBind.builtinsImport.uri.key; + if (this._viperBuiltinsVpMergedKeys.has(key)) { + return; + } + + // 先标记(防止绑定 builtins.vp 时递归进入本方法),成功后保留该标记。 + this._viperBuiltinsVpMergedKeys.add(key); + + // 在 workspace root / 项目 includes(extraPaths)中定位 builtins.vp。 + // 注意:用 fileToBind.uri 而非 builtinsImport.uri(后者位于 typeshed, + // findExecEnvironment 会落到默认环境而丢失 extraPaths)。 + const execEnv = this._configOptions.findExecEnvironment(fileToBind.uri); + const candidates: Uri[] = []; + if (execEnv?.root) { + candidates.push(execEnv.root); + } + if (execEnv) { + appendArray(candidates, execEnv.extraPaths); + } + + // Viper 扩展:真实编译器始终把 stdlib 的 includes 目录纳入编译链, + // 与工程是否在 .vpj / viperconfig 中声明 includes 无关。这里额外沿 + // "被分析文件" 与 "执行环境根" 的祖先目录上溯(最多 6 层),把每个 + // 祖先目录自身及其 includes/ 子目录也纳入 builtins.vp 的候选位置, + // 使 `/includes/builtins.vp` 对 `/Test/.../App/main.vp` + // 这类未显式配置 includes 的工程同样可用(对齐编译器行为)。 + const ancestorKeys = new Set(candidates.map((c) => c.key)); + const seeds: Uri[] = []; + if (fileToBind.uri && !fileToBind.uri.isEmpty()) { + seeds.push(fileToBind.uri.getDirectory()); + } + if (execEnv?.root) { + seeds.push(execEnv.root); + } + for (const seed of seeds) { + let dir: Uri | undefined = seed; + for (let depth = 0; depth < 6 && dir && !dir.isEmpty(); depth++) { + for (const candidateDir of [dir, dir.combinePathsUnsafe('includes')]) { + if (ancestorKeys.has(candidateDir.key)) { + continue; + } + ancestorKeys.add(candidateDir.key); + candidates.push(candidateDir); + } + const parent = dir.getDirectory(); + if (parent.equals(dir)) { + break; + } + dir = parent; + } + } + + let builtinsVpUri: Uri | undefined; + for (const dir of candidates) { + const candidate = dir.combinePathsUnsafe('builtins').addExtension('.vp'); + if (this.fileSystem.existsSync(candidate)) { + builtinsVpUri = candidate; + break; + } + } + + if (!builtinsVpUri) { + return; + } + + // 加载并绑定 builtins.vp(_lookUpImport 会按需绑定,递归安全)。 + this.addTrackedFile(builtinsVpUri); + this._lookUpImport(builtinsVpUri); + + const builtinsVpInfo = this.getSourceFileInfo(builtinsVpUri); + const parseResults = builtinsVpInfo?.sourceFile.getParserOutput(); + if (!builtinsVpInfo || !parseResults) { + return; + } + + const builtinsVpScope = AnalyzerNodeInfo.getScope(parseResults.parseTree); + if (!builtinsVpScope) { + return; + } + + // 合并:builtins.vp 的声明优先覆盖 builtin 作用域(完全对齐真实编译器, + // 如 ord/chr 的 t.CChar 签名、min/max 的 double 签名、rowptr/size_t 等 typedef)。 + // 覆盖范围:t.CTypedef 类型别名声明 + 函数声明;跳过模块别名(__stdint/t) + // 与隐式 dunder 符号,避免引入不必要的隐式全局。 + for (const [name, symbol] of builtinsVpScope.symbolTable) { + const decls = symbol.getDeclarations(); + const isViperDecl = decls.some( + (decl) => + (decl.type === DeclarationType.Variable && + !!decl.typeAnnotationNode && + decl.typeAnnotationNode.nodeType === ParseNodeType.MemberAccess && + (decl.typeAnnotationNode as MemberAccessNode).d.member.d.value === 'CTypedef') || + decl.type === DeclarationType.Function + ); + if (!isViperDecl) { + continue; + } + + builtinsScope.symbolTable.set(name, symbol); + } + } + private _getEffectiveFutureImports(futureImports: Set, chainedSourceFile: SourceFileInfo): Set { const effectiveFutureImports = new Set(futureImports); diff --git a/packages/vipeye-internal/src/analyzer/sourceFile.ts b/packages/vipeye-internal/src/analyzer/sourceFile.ts index c1e4601c..940f7dea 100644 --- a/packages/vipeye-internal/src/analyzer/sourceFile.ts +++ b/packages/vipeye-internal/src/analyzer/sourceFile.ts @@ -284,7 +284,7 @@ export class SourceFile { this._uri = uri; this._fileId = this._makeFileId(uri); this._moduleNameGetter = moduleNameGetter; - this._isStubFile = uri.hasExtension('.vpi'); + this._isStubFile = uri.hasExtension('.vpi') || uri.hasExtension('.vpnci'); this._isThirdPartyImport = isThirdPartyImport; this._isThirdPartyPyTypedPresent = isThirdPartyPyTypedPresent; const fileName = uri.fileName; diff --git a/packages/vipeye-internal/src/analyzer/sourceMapper.ts b/packages/vipeye-internal/src/analyzer/sourceMapper.ts index 40c71388..0e73aa74 100644 --- a/packages/vipeye-internal/src/analyzer/sourceMapper.ts +++ b/packages/vipeye-internal/src/analyzer/sourceMapper.ts @@ -1128,7 +1128,7 @@ export class SourceMapper { } export function isStubFile(uri: Uri): boolean { - return uri.lastExtension === '.vpi'; + return uri.lastExtension === '.vpi' || uri.lastExtension === '.vpnci'; } export function isAliasFactoryCallee(callTarget: ExpressionNode, allowMemberAccess = false): boolean { diff --git a/packages/vipeye-internal/src/analyzer/typeEvaluator.ts b/packages/vipeye-internal/src/analyzer/typeEvaluator.ts index dc2e7630..19d899cf 100644 --- a/packages/vipeye-internal/src/analyzer/typeEvaluator.ts +++ b/packages/vipeye-internal/src/analyzer/typeEvaluator.ts @@ -4900,6 +4900,23 @@ export function createTypeEvaluator( break; } + case ParseNodeType.UnaryOperation: { + // Viper 扩展:`*p = value`(C 指针解引用写回)。 + // 将 value 赋给 p 指向的类型(pointee)。仅 Viper 方言、一元乘法(解引用)。 + if ( + target.d.operator === OperatorType.Multiply && + IsViperDialect(AnalyzerNodeInfo.getFileInfo(target).executionEnvironment.dialect) + ) { + // 解引用目标类型 = p 指向的类型(getTypeOfViperDeref)。 + const derefTypeResult = getTypeOfExpression(target); + assignType(derefTypeResult.type, typeResult.type, expectedTypeDiagAddendum); + writeTypeCache(target, derefTypeResult, EvalFlags.None); + break; + } + addDiagnostic(DiagnosticRule.reportGeneralTypeIssues, LocMessage.assignmentTargetExpr(), target); + break; + } + default: { addDiagnostic(DiagnosticRule.reportGeneralTypeIssues, LocMessage.assignmentTargetExpr(), target); break; @@ -6218,7 +6235,15 @@ export function createTypeEvaluator( case TypeCategory.Never: { // Viper 扩展:Unknown 类型成员访问时标黄警告"对象可能未定义"。 // 主要用于未解析的导入模块(如 import serial 失败后 serial.puts 访问)。 - if (isUnknown(baseType) && IsViperDialect(fileInfo.executionEnvironment.dialect)) { + // 仅对"最终" Unknown(isIncomplete=false)报错:isIncomplete=true 表示 + // 类型分析中间态(依赖尚未收敛,如循环 import 中方法绑定未完成), + // 此时报"对象可能未定义"是误报(收敛后类型会修正,如 + // TransPyC StructGenMixin.get_mro_chain 内 self.find_struct)。 + if ( + isUnknown(baseType) && + !(baseType.priv.isIncomplete ?? false) && + IsViperDialect(fileInfo.executionEnvironment.dialect) + ) { fileInfo.diagnosticSink.addDiagnosticWithTextRange( 'warning', LocMessage.viperUnknownMemberAccess().format({ @@ -6381,9 +6406,19 @@ export function createTypeEvaluator( isClassInstance(baseType) && isViperREnumClass(baseType) ) { - const variantFieldType = getViperREnumVariantFieldType(baseType, memberName); - if (variantFieldType) { - typeResult = { type: variantFieldType }; + // Viper 扩展:REnum 自带 kind() 方法。 + // REnum 内存布局为 {i32 __tag, }, + // kind() 读取 __tag 判别值(对齐 TPC 的 _EmitREnumLlvm), + // 返回 t.CInt(与 ast.AST.kind() 一致)。 + if (memberName === 'kind') { + const kindFunctionType = FunctionType.createSynthesizedInstance('kind'); + kindFunctionType.shared.declaredReturnType = getBuiltInObject(node.d.member, 'int'); + typeResult = { type: kindFunctionType }; + } else { + const variantFieldType = getViperREnumVariantFieldType(baseType, memberName); + if (variantFieldType) { + typeResult = { type: variantFieldType }; + } } } } @@ -9324,6 +9359,39 @@ export function createTypeEvaluator( return undefined; } + // Viper 扩展:判断调用节点是否为内置 enumerate 调用。 + // Viper 语义下 enumerate 的迭代元素类型按 for 原迭代逻辑计算 + //(str/bytes = i8* → t.CChar、CPtr[T] → T),索引类型为 t.CSizeT(size_t)。 + function isViperEnumerateCall(node: CallNode, baseTypeResult: TypeResult): boolean { + return isInstantiableClass(baseTypeResult.type) && ClassType.isBuiltIn(baseTypeResult.type, 'enumerate'); + } + + // Viper 扩展:计算 enumerate(...) 迭代产生的元组类型 tuple[CSizeT, 元素类型]。 + // 若非 Viper 方言下的 enumerate 调用则返回 undefined。 + function getViperEnumerateIteratedType(expr: ExpressionNode): Type | undefined { + if (!IsViperDialect(AnalyzerNodeInfo.getFileInfo(expr).executionEnvironment.dialect)) { + return undefined; + } + if (expr.nodeType !== ParseNodeType.Call) { + return undefined; + } + const callNode = expr as CallNode; + const calleeTypeResult = getTypeOfExpression(callNode.d.leftExpr); + if (!isInstantiableClass(calleeTypeResult.type) || !ClassType.isBuiltIn(calleeTypeResult.type, 'enumerate')) { + return undefined; + } + const argExpr = callNode.d.args.length >= 1 ? callNode.d.args[0].d.valueExpr : undefined; + const elementType = argExpr + ? getTypeOfIterator(getTypeOfExpression(argExpr), /* isAsync */ false, argExpr)?.type ?? + UnknownType.create() + : UnknownType.create(); + const indexType = getViperSizeTType(expr) ?? getBuiltInObject(expr, 'int'); + return makeTupleObject(evaluatorInterface, [ + { type: indexType, isUnbounded: false }, + { type: elementType, isUnbounded: false }, + ]); + } + // Viper 扩展:判断调用节点是否为 stdarg.va_arg 调用。 // va_arg 通过 `from stdarg import va_arg` 引入,baseTypeResult 为 FunctionType, // 其 shared.name 为 'va_arg'。TPV 将 va_arg 视为 builtin,第二参数是类型名。 @@ -9839,6 +9907,27 @@ export function createTypeEvaluator( } else { typeResult.type = getViperSizeTType(node) ?? getBuiltInObject(node, 'int'); } + } else if ( + // Viper 扩展:拦截内置 enumerate 调用。 + // Python 声明 `enumerate(iterable: Iterable[_T]) -> enumerate[_T]`, + // 但 str/bytes(i8*)实参无法满足 Iterable[_T] 协议匹配, + // 导致 _T 求解为 Unknown(`for i, ch in enumerate(str)` 中 i/ch 均变 Unknown)。 + // 此处按 for 原迭代逻辑(getTypeOfIterator)计算元素类型并直接特化 enumerate: + // str/bytes → t.CChar、CPtr[T] → T 等。索引类型由 for 语句特判 + //(getViperEnumerateIteratedType)提供 t.CSizeT。 + IsViperDialect(AnalyzerNodeInfo.getFileInfo(node).executionEnvironment.dialect) && + isViperEnumerateCall(node, baseTypeResult) + ) { + const argExpr = node.d.args.length >= 1 ? node.d.args[0].d.valueExpr : undefined; + const argTypeResult = argExpr ? getTypeOfExpression(argExpr) : undefined; + const elementType = argTypeResult + ? getTypeOfIterator(argTypeResult, /* isAsync */ false, argExpr!)?.type ?? UnknownType.create() + : UnknownType.create(); + const enumerateType = getBuiltInType(node, 'enumerate'); + typeResult.type = + isInstantiableClass(enumerateType) && elementType + ? ClassType.cloneAsInstance(ClassType.specialize(enumerateType, [elementType])) + : UnknownType.create(); } else { const callResult = validateCallArgs( node, @@ -9902,6 +9991,40 @@ export function createTypeEvaluator( typeResult = { type: UnknownType.create() }; } + // Viper 扩展:返回类型为 t.State 的函数是 C 风格 noreturn(仅声明不定义, + // 如 sys.exit / stdlib.exit 的 `-> t.State`)。调用此类函数后流程终止(Never), + // 使 `if x is None: fatal_error(...)` 模式能正确窄化——fatal_error 内部调用 + // sys.exit(1) 返回 Never 后,fatal_error 被推断为 NoReturn,if 的 true 分支 + // 终止流程,后续代码不会错误地保留 None 分支(否则 `x.Ty.Pointee` 会把 + // x.Ty 解析为 `None | CPtr[LLVMType]` 并误报 "None 没有属性 Pointee")。 + if ( + IsViperDialect(AnalyzerNodeInfo.getFileInfo(node).executionEnvironment.dialect) && + !typeResult.typeErrors && + !isNever(typeResult.type) && + isFunctionOrOverloaded(baseTypeResult.type) + ) { + let hasViperStateReturn = false; + doForEachSubtype(baseTypeResult.type, (subtype) => { + if (isFunction(subtype)) { + const declaredReturn = subtype.shared.declaredReturnType; + if (declaredReturn) { + doForEachSubtype(declaredReturn, (retSubtype) => { + if ( + isClass(retSubtype) && + isCTypeSubclassType(retSubtype) && + retSubtype.shared.name === 'State' + ) { + hasViperStateReturn = true; + } + }); + } + } + }); + if (hasViperStateReturn) { + typeResult.type = NeverType.createNever(); + } + } + return typeResult; } @@ -18366,6 +18489,17 @@ export function createTypeEvaluator( EvalFlags.NoTypeVarTuple | EvalFlags.NoClassVar; + // Viper 扩展:保留上方"裸类引用 RHS 不专用化"的 NoSpecialize 标志。 + // 否则 `rowptr: t.CTypedef = t.CPtr` 的 RHS 会被 reportMissingTypeArgs + // 预填成 CPtr[Unknown],导致 `rowptr[int]` 无法再特化 + // (下标处报"类型 CPtr[Unknown] 已专用化")。 + if ( + node.d.rightExpr.nodeType === ParseNodeType.Name || + node.d.rightExpr.nodeType === ParseNodeType.MemberAccess + ) { + flags |= EvalFlags.NoSpecialize; + } + typeAliasNameNode = (node.d.leftExpr as TypeAnnotationNode).d.valueExpr as NameNode; if (!isLegalTypeAliasExpressionForm(node.d.rightExpr, /* allowStrLiteral */ true)) { @@ -21337,8 +21471,18 @@ export function createTypeEvaluator( } const iteratorTypeResult = getTypeOfExpression(node.d.iterableExpr); - const iteratedType = - getTypeOfIterator(iteratorTypeResult, !!node.d.isAsync, node.d.iterableExpr)?.type ?? UnknownType.create(); + let iteratedType: Type; + // Viper 扩展:`for i, ch in enumerate(x)` 中 enumerate 迭代产生 + // tuple[t.CSizeT, 元素类型] —— 索引为 size_t(C 语义数组下标), + // 元素类型按 for 原迭代逻辑计算(str/bytes → t.CChar、CPtr[T] → T)。 + const enumerateIteratedType = getViperEnumerateIteratedType(node.d.iterableExpr); + if (enumerateIteratedType) { + iteratedType = enumerateIteratedType; + } else { + iteratedType = + getTypeOfIterator(iteratorTypeResult, !!node.d.isAsync, node.d.iterableExpr)?.type ?? + UnknownType.create(); + } assignTypeToExpression( node.d.targetExpr, @@ -26422,15 +26566,25 @@ export function createTypeEvaluator( } } - // Viper 扩展:整数 ↔ 指针隐式转换语义。 - // 在 C/Viper 语义中,整数(地址值)与指针类型可相互转换: - // - 整数地址 → CPtr/CVoid(如 framebuffer_addr: CUnsignedLong 赋值给 CVoid | CPtr) - // - 指针 → 整数(如 uintptr_t = (uintptr_t)ptr) - // 这模拟 C 语言的整数-指针转换规则。 + // Viper 扩展:整数 ↔ 指针隐式转换语义(C 空指针常量规则)。 + // C 语言中只有"值为 0 的整型常量表达式"(null pointer constant)可以 + // 隐式转换为任意指针类型(NULL);其他整数值(如地址值)必须显式转换。 + // - 整数字面量 0 → CPtr/CVoid:允许(NULL 空指针) + // - 其他整数 → 指针:不允许,需显式转换(如 t.CUInt32T(addr, t.CPtr)) + // - 指针 → 整数:允许(如 uintptr_t = (uintptr_t)ptr) const destIsPtrLike = isCPtrType(destType) || isCVoidType(destType); const srcIsPtrLike = isCPtrType(srcType) || isCVoidType(srcType); if (destIsPtrLike && isViperIntegerType(srcType)) { - return true; + // 仅整数字面量 0(null pointer constant)可隐式转换为指针; + // 变量/常量即使值为 0 也不是字面量(C 语义同样拒绝)。 + if (isClassInstance(srcType) && srcType.priv.literalValue === 0) { + return true; + } + diag?.addMessage( + `仅允许整数字面量 0 隐式转换为指针(NULL 空指针),` + + `其他整数值需显式转换(如 t.CInt(值, t.CPtr))` + ); + return false; } if (srcIsPtrLike && isViperIntegerType(destType)) { return true; diff --git a/packages/vipeye-internal/src/analyzer/typeGuards.ts b/packages/vipeye-internal/src/analyzer/typeGuards.ts index 884cfea1..fb799e9c 100644 --- a/packages/vipeye-internal/src/analyzer/typeGuards.ts +++ b/packages/vipeye-internal/src/analyzer/typeGuards.ts @@ -10,6 +10,7 @@ */ import { assert } from '../common/debug'; +import { IsViperDialect } from '../common/dialect'; import { ArgCategory, AssignmentExpressionNode, @@ -45,6 +46,7 @@ import { isAnyOrUnknown, isClass, isClassInstance, + isCPtrType, isFunction, isFunctionOrOverloaded, isInstantiableClass, @@ -175,7 +177,11 @@ export function getTypeNarrowingCallback( ) ) { return (type: Type) => { - return { type: narrowTypeForIsNone(evaluator, type, adjIsPositiveTest), isIncomplete: false }; + const isViperDialect = IsViperDialect(getFileInfo(testExpression).executionEnvironment?.dialect); + return { + type: narrowTypeForIsNone(evaluator, type, adjIsPositiveTest, isViperDialect), + isIncomplete: false, + }; }; } @@ -1108,7 +1114,12 @@ function getInnermostNewTypeBaseInstance(subtype: Type): Type | undefined { } // Handle type narrowing for expressions of the form "x is None" and "x is not None". -function narrowTypeForIsNone(evaluator: TypeEvaluator, type: Type, isPositiveTest: boolean) { +function narrowTypeForIsNone( + evaluator: TypeEvaluator, + type: Type, + isPositiveTest: boolean, + isViperDialect = false +) { const expandedType = mapSubtypes(type, (subtype) => { return transformPossibleRecursiveTypeAlias(subtype); }); @@ -1172,6 +1183,15 @@ function narrowTypeForIsNone(evaluator: TypeEvaluator, type: Type, isPositiveTes return isPositiveTest ? adjustedSubtype : undefined; } + // Viper 扩展:None 即 C 空指针 NULL,是 CPtr 指针类型的一个取值,而非独立类型。 + // `p is None` 的正测试只是空指针判断,不改变指针类型本身(与 C 语义一致)。 + // 若不拦截,正分支会把 CPtr[T] 窄化为 NoneType,随后与负分支合并时 + // combineTypes 会把 None 误当作结构体候选折叠成 CPtr[None], + // 导致 `if p is None: fatal_error(...)` 守卫之后 `.Pointee` 等成员访问误报。 + if (isViperDialect && isClassInstance(subtype) && isCPtrType(subtype)) { + return adjustedSubtype; + } + // Is it potentially None? if (evaluator.assignType(subtype, evaluator.getNoneType())) { resultIncludesNoneSubtype = true; diff --git a/packages/vipeye-internal/src/analyzer/types.ts b/packages/vipeye-internal/src/analyzer/types.ts index dcc82d92..373dce43 100644 --- a/packages/vipeye-internal/src/analyzer/types.ts +++ b/packages/vipeye-internal/src/analyzer/types.ts @@ -4153,7 +4153,8 @@ export function combineTypes(subtypes: Type[], options?: CombineTypesOptions): T // Viper 扩展:将 Struct | CPtr 自动映射为 CPtr[Struct]。 // 模拟 C 语言指针语义:结构体与指针的联合类型自动转换为指向该结构体的指针类型。 // 例如:Scheduler | t.CPtr → CPtr[Scheduler],bootinfo.bootinfo | t.CPtr → CPtr[bootinfo.bootinfo]。 - // 识别条件:联合类型中同时存在 CPtr 子类型和"结构体"子类型(非 CPtr/CVoid/CArray/CConst/CDefine/整数/str/bytes)。 + // 识别条件:联合类型中同时存在 CPtr 子类型和"结构体"子类型(非 CPtr/CVoid/CArray/CConst/CDefine/str/bytes)。 + // int 允许作为结构体候选:`int | t.CPtr` → CPtr[int](C 的 int*),与 `t.CInt | t.CPtr` 一致。 if (subtypes.length > 1) { let cptrSubtype: ClassType | undefined; const structSubtypes: ClassType[] = []; @@ -4167,12 +4168,13 @@ export function combineTypes(subtypes: Type[], options?: CombineTypesOptions): T } } else if ( isClass(subtype) && + // None 是空指针取值而非结构体,不能作为 CPtr 的类型参数(否则 None | CPtr[T] 会误折叠成 CPtr[None]) + !ClassType.isBuiltIn(subtype, 'NoneType') && !isCVoidType(subtype) && !isCArrayType(subtype) && !isCConstType(subtype) && !isCStaticType(subtype) && !isCDefineType(subtype) && - !ClassType.isBuiltIn(subtype, 'int') && !ClassType.isBuiltIn(subtype, 'str') && !ClassType.isBuiltIn(subtype, 'bytes') && !ClassType.isBuiltIn(subtype, 'object') diff --git a/packages/vipeye-internal/src/analyzer/typeshedInfoProvider.ts b/packages/vipeye-internal/src/analyzer/typeshedInfoProvider.ts index e6ad813e..eab9ebda 100644 --- a/packages/vipeye-internal/src/analyzer/typeshedInfoProvider.ts +++ b/packages/vipeye-internal/src/analyzer/typeshedInfoProvider.ts @@ -118,7 +118,7 @@ class DefaultTypeshedInfoProvider implements TypeshedInfoProvider { typeshedThirdPartyPackagePaths.set(innerEntry.name, [innerDirPath]); } } else if (innerEntry.isFile()) { - if (innerEntry.name.endsWith('.vpi')) { + if (innerEntry.name.endsWith('.vpi') || innerEntry.name.endsWith('.vpnci')) { const strippedFileName = stripFileExtension(innerEntry.name); const pathList = typeshedThirdPartyPackagePaths.get(strippedFileName); if (pathList) { diff --git a/packages/vipeye-internal/src/common/languageInfoUtils.ts b/packages/vipeye-internal/src/common/languageInfoUtils.ts index c538ae13..f2ac3518 100644 --- a/packages/vipeye-internal/src/common/languageInfoUtils.ts +++ b/packages/vipeye-internal/src/common/languageInfoUtils.ts @@ -1229,6 +1229,8 @@ function getKeywordTypeString(type: KeywordType) { return 'Debug'; case KeywordType.Def: return 'Def'; + case KeywordType.Define: + return 'Define'; case KeywordType.Del: return 'Del'; case KeywordType.Elif: diff --git a/packages/vipeye-internal/src/common/pathUtils.ts b/packages/vipeye-internal/src/common/pathUtils.ts index 1929185d..623b964b 100644 --- a/packages/vipeye-internal/src/common/pathUtils.ts +++ b/packages/vipeye-internal/src/common/pathUtils.ts @@ -606,7 +606,7 @@ export function getWildcardRoot(rootPath: string, fileSpec: string): string { } export function hasPythonExtension(path: string) { - return path.endsWith('.vp') || path.endsWith('.vpi'); + return path.endsWith('.vp') || path.endsWith('.vpi') || path.endsWith('.vpnci'); } export function getRegexEscapedSeparator(pathSep: string = path.sep) { diff --git a/packages/vipeye-internal/src/common/uri/uriUtils.ts b/packages/vipeye-internal/src/common/uri/uriUtils.ts index 290b5600..e7cafdf1 100644 --- a/packages/vipeye-internal/src/common/uri/uriUtils.ts +++ b/packages/vipeye-internal/src/common/uri/uriUtils.ts @@ -334,7 +334,7 @@ export function getWildcardRoot(root: Uri, fileSpec: string): Uri { } export function hasPythonExtension(uri: Uri) { - return uri.hasExtension('.vp') || uri.hasExtension('.vpi'); + return uri.hasExtension('.vp') || uri.hasExtension('.vpi') || uri.hasExtension('.vpnci'); } export function getFileSpec(root: Uri, fileSpec: string): FileSpec { diff --git a/packages/vipeye-internal/src/languageService/autoImporter.ts b/packages/vipeye-internal/src/languageService/autoImporter.ts index 5fd2f571..879cd247 100644 --- a/packages/vipeye-internal/src/languageService/autoImporter.ts +++ b/packages/vipeye-internal/src/languageService/autoImporter.ts @@ -479,8 +479,9 @@ export class AutoImporter { const fileDir = uri.getDirectory(); const initPathPy = fileDir.initPyUri; const initPathPyi = fileDir.initPyiUri; - const isStub = uri.hasExtension('.vpi'); - const hasInit = map.has(initPathPy.key) || map.has(initPathPyi.key); + const initPathVpnci = fileDir.combinePathsUnsafe('__init__.vpnci'); + const isStub = uri.hasExtension('.vpi') || uri.hasExtension('.vpnci'); + const hasInit = map.has(initPathPy.key) || map.has(initPathPyi.key) || map.has(initPathVpnci.key); const sourceFileInfo = this.program.getSourceFileInfo(uri); return { isStub, hasInit, isUserCode: isUserCode(sourceFileInfo) }; } diff --git a/packages/vipeye-internal/src/languageService/hoverProvider.ts b/packages/vipeye-internal/src/languageService/hoverProvider.ts index ee3ef0ee..ca76aeea 100644 --- a/packages/vipeye-internal/src/languageService/hoverProvider.ts +++ b/packages/vipeye-internal/src/languageService/hoverProvider.ts @@ -47,7 +47,16 @@ import { convertOffsetToPosition, convertPositionToOffset } from '../common/posi import { ServiceProvider } from '../common/serviceProvider'; import { Position, Range, TextRange } from '../common/textRange'; import { Uri } from '../common/uri/uri'; -import { ExpressionNode, FunctionNode, NameNode, ParseNode, ParseNodeType, StringNode } from '../parser/parseNodes'; +import { + ExpressionNode, + FunctionNode, + NameNode, + ParamCategory, + ParameterNode, + ParseNode, + ParseNodeType, + StringNode, +} from '../parser/parseNodes'; import { ParseFileResults } from '../parser/parser'; import { TokenType } from '../parser/tokenizerTypes'; import { @@ -600,6 +609,10 @@ export class HoverProvider { ); this._addResultsPart(parts, signatureString, /* python */ true); + + // Viper 扩展:hover 函数时附加详尽调试信息(仅 Viper 方言)。 + this._addViperFunctionDebugPart(parts, resolvedDecl); + this._addDocumentationPart(parts, node, resolvedDecl); break; } @@ -624,6 +637,120 @@ export class HoverProvider { } } + // Viper 扩展:hover 函数时附加详尽调试信息(仅 Viper 方言)。 + // 显示:定义位置、修饰符(extern/export/inline)、泛型参数、参数列表(类型/默认值)、返回类型。 + private _addViperFunctionDebugPart(parts: HoverTextPart[], resolvedDecl: FunctionDeclaration): void { + const funcNode = resolvedDecl.node; + if (funcNode.nodeType !== ParseNodeType.Function) { + return; + } + + const fileInfo = getFileInfo(funcNode); + if (!IsViperDialect(fileInfo.executionEnvironment.dialect)) { + return; + } + // 不对 stub 文件中的声明附加调试信息(如 stdlib .vpi / typeshed .pyi 类型定义), + // 避免对纯声明函数产生噪声。 + const declPathLower = fileInfo.fileUri.getPath().toLowerCase(); + if (declPathLower.endsWith('.pyi') || declPathLower.endsWith('.vpi') || declPathLower.endsWith('.vpnci')) { + return; + } + + const declUri = fileInfo.fileUri; + const declParseResults = this._program.getParseResults(declUri); + if (!declParseResults) { + return; + } + const declText = declParseResults.text; + const declLines = declParseResults.parserOutput.lines; + + const isMarkdown = this._format === MarkupKind.Markdown; + const code = (s: string) => (isMarkdown ? `\`${s}\`` : s); + const bold = (s: string) => (isMarkdown ? `**${s}**` : s); + const lines: string[] = []; + + // 定义位置(文件:行:列) + const namePos = convertOffsetToPosition(funcNode.d.name.start, declLines); + const locDisplay = declUri.key === this._fileUri.key ? '此文件' : declUri.getPath(); + lines.push(`${bold('位置')}: ${code(`${locDisplay}:${namePos.line + 1}:${namePos.character + 1}`)}`); + + // 修饰符(extern/export/inline 等 C 修饰关键字) + if (funcNode.d.viperModifier) { + lines.push(`${bold('修饰符')}: ${code(funcNode.d.viperModifier)}`); + } + + // 泛型参数(PEP 695 type 参数) + if (funcNode.d.typeParams && funcNode.d.typeParams.d.params.length > 0) { + const typeParamNames = funcNode.d.typeParams.d.params.map((p) => p.d.name.d.value).join(', '); + lines.push(`${bold('泛型参数')}: ${code(`[${typeParamNames}]`)}`); + } + + // 参数列表(名称、类型、默认值、*args/**kwargs 类别) + if (funcNode.d.params.length > 0) { + lines.push(`${bold('参数')}:`); + for (const param of funcNode.d.params) { + const paramText = this._getViperParamDebugText(param, declText); + if (paramText) { + lines.push(` ${code(paramText)}`); + } + } + } + + // 返回类型 + const returnType = this._getViperFunctionReturnType(funcNode); + if (returnType) { + lines.push(`${bold('返回类型')}: ${code(this._evaluator.printType(convertToInstance(returnType)))}`); + } + + if (lines.length === 0) { + return; + } + + const heading = isMarkdown ? '**函数调试信息**' : '函数调试信息'; + parts.push({ text: heading + '\n' + lines.join('\n'), python: false }); + } + + // 构造单个参数的调试文本:`*名称: 类型 = 默认值`。 + private _getViperParamDebugText(param: ParameterNode, declText: string): string | undefined { + let text = ''; + if (param.d.category === ParamCategory.ArgsList) { + text += '*'; + } else if (param.d.category === ParamCategory.KwargsDict) { + text += '**'; + } + text += param.d.name?.d.value ?? '(匿名)'; + + let paramType: Type | undefined; + if (param.d.annotation) { + paramType = this._evaluator.getTypeOfAnnotation(param.d.annotation); + } else if (param.d.name) { + paramType = this._evaluator.getTypeOfExpression(param.d.name).type; + } + if (paramType) { + text += ': ' + this._evaluator.printType(convertToInstance(paramType)); + } + + if (param.d.defaultValue) { + const defaultText = declText.slice(param.d.defaultValue.start, TextRange.getEnd(param.d.defaultValue)); + text += ` = ${defaultText}`; + } + + return text; + } + + // 返回类型:优先取注解类型,无注解时从函数类型的声明/推断返回类型提取。 + private _getViperFunctionReturnType(funcNode: FunctionNode): Type | undefined { + if (funcNode.d.returnAnnotation) { + return this._evaluator.getTypeOfAnnotation(funcNode.d.returnAnnotation); + } + + const fnType = this._getType(funcNode.d.name); + if (isFunction(fnType)) { + return fnType.shared.declaredReturnType ?? fnType.shared.inferredReturnType?.type; + } + return undefined; + } + private _addResultsForSynthesizedType(parts: HoverTextPart[], typeInfo: SynthesizedTypeInfo, hoverNode: NameNode) { let typeText: string | undefined; diff --git a/packages/vipeye-internal/src/languageService/semanticTokenProvider.ts b/packages/vipeye-internal/src/languageService/semanticTokenProvider.ts index 7e5d9189..14f25b00 100644 --- a/packages/vipeye-internal/src/languageService/semanticTokenProvider.ts +++ b/packages/vipeye-internal/src/languageService/semanticTokenProvider.ts @@ -15,7 +15,7 @@ */ import * as AnalyzerNodeInfo from '../analyzer/analyzerNodeInfo'; -import { Declaration, DeclarationType, VariableDeclaration } from '../analyzer/declaration'; +import { Declaration, DeclarationType, FunctionDeclaration, VariableDeclaration } from '../analyzer/declaration'; import { ParseTreeWalker } from '../analyzer/parseTreeWalker'; import { isWithinTypeAnnotation } from '../analyzer/parseTreeUtils'; import { TypeCategory, isCDefineType } from '../analyzer/types'; @@ -619,7 +619,14 @@ class SemanticTokenWalker extends ParseTreeWalker { memberAccessType.category !== TypeCategory.Unknown && memberAccessType.category !== TypeCategory.Unbound ) { - tokenType = SemanticTokenTypes.property; + // Viper 扩展:成员访问可求值时,若为函数类型(如 REnum 自带 + // kind()、CPtr 解引用转发的方法)显示为方法(函数颜色); + // 数据字段显示为 property。 + tokenType = + memberAccessType.category === TypeCategory.Function || + memberAccessType.category === TypeCategory.Overloaded + ? SemanticTokenTypes.function + : SemanticTokenTypes.property; } else { tokenType = SemanticTokenTypes.undefined; } @@ -647,7 +654,12 @@ class SemanticTokenWalker extends ParseTreeWalker { tokenModifiers |= defMod; break; case DeclarationType.Function: - tokenType = SemanticTokenTypes.function; + // Viper 扩展:define 宏函数名(定义处与调用处)显示为 macro(深蓝色)。 + if ((decl as FunctionDeclaration).node.d.isDefine) { + tokenType = SemanticTokenTypes.macro; + } else { + tokenType = SemanticTokenTypes.function; + } tokenModifiers |= defMod; break; case DeclarationType.Param: diff --git a/packages/vipeye-internal/src/languageService/semanticTokenTypes.ts b/packages/vipeye-internal/src/languageService/semanticTokenTypes.ts index 4c705e41..3fb3d0e0 100644 --- a/packages/vipeye-internal/src/languageService/semanticTokenTypes.ts +++ b/packages/vipeye-internal/src/languageService/semanticTokenTypes.ts @@ -38,6 +38,8 @@ export const enum SemanticTokenTypes { decorator = 12, /** 属性 */ property = 13, + /** 宏函数(define 定义的编译期宏,显示为深蓝色) */ + macro = 14, } /** 语义 token 修饰符(位掩码) */ @@ -70,6 +72,7 @@ export const semanticTokenTypesLegend: string[] = [ 'undefined', 'decorator', 'property', + 'macro', ]; /** LSP legend 中的 tokenModifiers 字符串列表(顺序与 bit 位对应) */ diff --git a/packages/vipeye-internal/src/localization/localize.ts b/packages/vipeye-internal/src/localization/localize.ts index 18789381..46ecc236 100644 --- a/packages/vipeye-internal/src/localization/localize.ts +++ b/packages/vipeye-internal/src/localization/localize.ts @@ -914,6 +914,7 @@ export namespace Localizer { export const returnTypeUnknown = () => getRawString('Diagnostic.returnTypeUnknown'); export const returnTypePartiallyUnknown = () => new ParameterizedString<{ returnType: string }>(getRawString('Diagnostic.returnTypePartiallyUnknown')); + export const viperReturnExprInNoneFunction = () => getRawString('Diagnostic.viperReturnExprInNoneFunction'); export const revealLocalsArgs = () => getRawString('Diagnostic.revealLocalsArgs'); export const revealLocalsNone = () => getRawString('Diagnostic.revealLocalsNone'); export const revealTypeArgs = () => getRawString('Diagnostic.revealTypeArgs'); 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 b3f860a5..9a3896ac 100644 --- a/packages/vipeye-internal/src/localization/package.nls.en-us.json +++ b/packages/vipeye-internal/src/localization/package.nls.en-us.json @@ -1169,6 +1169,7 @@ "returnTypeMismatch": "Type \"{exprType}\" is not assignable to return type \"{returnType}\"", "returnTypePartiallyUnknown": "Return type, \"{returnType}\", is partially unknown", "returnTypeUnknown": "Return type is unknown", + "viperReturnExprInNoneFunction": "A Viper function that returns None (no return value) cannot \"return\" an expression whose value is None; only a bare \"return\" or \"return None\" is allowed", "revealLocalsArgs": { "message": "Expected no arguments for \"reveal_locals\" call", "comment": "{Locked='reveal_locals'}" 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 024c230d..04259a76 100644 --- a/packages/vipeye-internal/src/localization/package.nls.zh-cn.json +++ b/packages/vipeye-internal/src/localization/package.nls.zh-cn.json @@ -455,6 +455,7 @@ "returnTypeMismatch": "类型“{exprType}”不可分配给返回类型“{returnType}”", "returnTypePartiallyUnknown": "返回类型“{returnType}”部分未知", "returnTypeUnknown": "返回类型未知", + "viperReturnExprInNoneFunction": "返回 None(无返回值)的函数不能用 return 返回值为 None 的表达式,只有裸 return 和 return None 合法", "revealLocalsArgs": "“reveal_locals”调用应没有参数", "revealLocalsNone": "此范围内没有 locals 变量", "revealTypeArgs": "“reveal_type”调用应为单个位置参数", diff --git a/packages/vipeye-internal/src/parser/parseNodes.ts b/packages/vipeye-internal/src/parser/parseNodes.ts index 45acfe1a..d3c76cc9 100644 --- a/packages/vipeye-internal/src/parser/parseNodes.ts +++ b/packages/vipeye-internal/src/parser/parseNodes.ts @@ -473,6 +473,8 @@ export interface FunctionNode extends ParseNodeBase { suite: SuiteNode; // Viper 扩展:函数声明前的 C 修饰关键字(inline/export/extern),用于语义校验。 viperModifier: string | undefined; + // Viper 扩展:define 宏函数标记(语法与 def 完全一致,语义为编译期宏展开)。 + isDefine: boolean; }; } @@ -482,7 +484,8 @@ export namespace FunctionNode { name: NameNode, suite: SuiteNode, typeParams?: TypeParameterListNode, - viperModifier?: string + viperModifier?: string, + isDefine?: boolean ) { const node: FunctionNode = { start: defToken.start, @@ -502,6 +505,7 @@ export namespace FunctionNode { funcAnnotationComment: undefined, suite: suite, viperModifier, + isDefine: isDefine ?? false, }, }; diff --git a/packages/vipeye-internal/src/parser/parser.ts b/packages/vipeye-internal/src/parser/parser.ts index 5a7014ac..38b86e01 100644 --- a/packages/vipeye-internal/src/parser/parser.ts +++ b/packages/vipeye-internal/src/parser/parser.ts @@ -488,6 +488,10 @@ export class Parser { case KeywordType.Def: return this._parseFunctionDef(); + case KeywordType.Define: + // Viper 扩展:define 宏函数(与 def 语法一致,语义为编译期宏展开)。 + return this._parseFunctionDef(); + case KeywordType.Class: return this._parseClassDef(); @@ -615,7 +619,9 @@ export class Parser { const wasParsingTypeAnnotation = this._isParsingTypeAnnotation; this._isParsingTypeAnnotation = true; - const expression = this._parseTestExpression(/* allowAssignmentExpression */ false); + let expression = this._parseTestExpression(/* allowAssignmentExpression */ false); + // Viper 扩展:类型别名同样支持 C 指针类型后缀(type P = T*)。 + expression = this._parseViperPointerTypeSuffix(expression); this._isParsingTypeAnnotation = wasParsingTypeAnnotation; return TypeAliasNode.create(typeToken, name, expression, typeParameters); @@ -2075,7 +2081,23 @@ export class Parser { ): FunctionNode | ErrorNode { // Viper 扩展:extern/export 直接声明形式(`extern A(...)`)没有 def 关键字, // 用修饰关键字 token 作为范围起点;否则正常消费 Def 关键字 token。 - const defToken = defTokenOverride ?? this._getKeywordToken(KeywordType.Def); + // define 宏函数关键字与 def 语义一致,语法完全相同,仅标记 isDefine。 + let defToken: Token; + let isDefine = false; + if (defTokenOverride) { + defToken = defTokenOverride; + } else { + const curToken = this._peekToken(); + if ( + curToken.type === TokenType.Keyword && + (curToken as KeywordToken).keywordType === KeywordType.Define + ) { + isDefine = true; + defToken = this._getNextToken() as KeywordToken; + } else { + defToken = this._getKeywordToken(KeywordType.Def); + } + } const nameToken = this._getTokenIfIdentifier(); if (!nameToken) { @@ -2156,7 +2178,7 @@ export class Parser { this._isInFinallyBlock = wasInFinallyBlock; this._isInFinallyLoop = wasInFinallyLoop; - const functionNode = FunctionNode.create(defToken, NameNode.create(nameToken), suite, typeParameters); + const functionNode = FunctionNode.create(defToken, NameNode.create(nameToken), suite, typeParameters, viperModifier, isDefine); functionNode.d.viperModifier = viperModifier; if (asyncToken) { functionNode.d.isAsync = true; @@ -2538,6 +2560,9 @@ export class Parser { } } else if (nextToken.keywordType === KeywordType.Def) { return this._parseFunctionDef(undefined, decoratorList); + } else if (nextToken.keywordType === KeywordType.Define) { + // Viper 扩展:装饰的 define 宏函数。 + return this._parseFunctionDef(undefined, decoratorList); } else if (nextToken.keywordType === KeywordType.Class) { return this._parseClassDef(decoratorList); } @@ -3388,12 +3413,40 @@ export class Parser { // test_or_star: test | star_expr private _parseTestOrStarExpression(allowAssignmentExpression: boolean): ExpressionNode { if (this._peekOperatorType() === OperatorType.Multiply) { + // Viper 扩展:前缀 '*' 在表达式位置可能是 C 风格一元解引用(`*p`), + // 也可能是解包元素(`[*a]`、`(*p, q)` 中的 `*a`/`*p`)。 + // 先按一元解引用解析,若后随逗号/闭括号等分隔符(解包元素上下文)则改判为 Unpack。 + if (this._parseOptions.dialect === 'viper') { + return this._parseViperStarExprOrDeref(); + } return this._parseExpression(/* allowUnpack */ true); } return this._parseTestExpression(allowAssignmentExpression); } + // Viper 扩展:解析前缀 '*'。先消费 '*' 并以非解包模式解析其操作数, + // 若操作数后紧跟逗号/闭括号等(表示它是解包元素),改判为 Unpack 节点; + // 否则构造一元 '*'(解引用)节点,与 C 语义一致。 + // 注意:`)` 不触发改判——`(*p)` 是单元素括号(解引用),而 `(*p,)`/`(*p, q)` + // 的 `*p` 后是逗号(解包元素)。 + private _parseViperStarExprOrDeref(): ExpressionNode { + const startToken = this._peekToken(); + this._getNextToken(); // 消费 '*' + const expression = this._parseExpression(/* allowUnpack */ false); + + const nextType = this._peekToken().type; + if ( + nextType === TokenType.Comma || + nextType === TokenType.CloseBracket || + nextType === TokenType.CloseCurlyBrace + ) { + return UnpackNode.create(startToken, expression); + } + + return this._createUnaryOperationNode(startToken, expression, OperatorType.Multiply); + } + // test: or_test ['if' or_test 'else' test] | lambdef private _parseTestExpression(allowAssignmentExpression: boolean): ExpressionNode { if (this._peekKeywordType() === KeywordType.Lambda) { @@ -3672,6 +3725,17 @@ export class Parser { nextOperator === OperatorType.Mod || nextOperator === OperatorType.FloorDivide ) { + // Viper 扩展:类型注解上下文中的后缀 '*' 是 C 指针类型(T* = CPtr[T]), + // 不是二元乘法。停止解析,交由 _parseTypeAnnotation/_parseTypeAliasStatement + // 的后缀处理消费,避免 `p: T*` 被解析成 `T * <缺失操作数>` 报错。 + if ( + this._parseOptions.dialect === 'viper' && + this._isParsingTypeAnnotation && + nextOperator === OperatorType.Multiply + ) { + break; + } + this._getNextToken(); const rightExpr = this._parseArithmeticFactor(); leftExpr = this._createBinaryOperationNode(leftExpr, rightExpr, peekToken, nextOperator); @@ -3682,15 +3746,21 @@ export class Parser { return leftExpr; } - // factor: ('+'|'-'|'~') factor | power + // factor: ('+'|'-'|'~'|'*'|'&') factor | power // power: atom_expr ['**' factor] + // Viper 扩展:一元 '*'(解引用)与 '&'(取地址)与 C 语义一致, + // 仅在 Viper 方言下将前缀 '*'/'&' 解析为一元运算。 private _parseArithmeticFactor(): ExpressionNode { const nextToken = this._peekToken(); const nextOperator = this._peekOperatorType(); + const isViperUnaryOp = + this._parseOptions.dialect === 'viper' && + (nextOperator === OperatorType.Multiply || nextOperator === OperatorType.BitwiseAnd); if ( nextOperator === OperatorType.Add || nextOperator === OperatorType.Subtract || - nextOperator === OperatorType.BitwiseInvert + nextOperator === OperatorType.BitwiseInvert || + isViperUnaryOp ) { this._getNextToken(); const expression = this._parseArithmeticFactor(); @@ -3703,7 +3773,15 @@ export class Parser { } const peekToken = this._peekToken(); - if (this._consumeTokenIfOperator(OperatorType.Power)) { + const nextPowerOperator = this._peekOperatorType(); + // Viper 扩展:类型注解上下文中的 '**' 是二级指针类型后缀(T** = CPtr[CPtr[T]]), + // 词法化为 Power 算子('**'),不是幂运算。跳过幂运算分支, + // 交由 _parseTypeAnnotation/_parseTypeAliasStatement 的后缀处理消费。 + if ( + nextPowerOperator === OperatorType.Power && + !(this._parseOptions.dialect === 'viper' && this._isParsingTypeAnnotation) + ) { + this._getNextToken(); const rightExpr = this._parseArithmeticFactor(); return this._createBinaryOperationNode(leftExpr, rightExpr, peekToken, OperatorType.Power); } @@ -4643,8 +4721,10 @@ export class Parser { // Is this a type annotation assignment? if (this._consumeTokenIfType(TokenType.Colon)) { + // 保留原始赋值目标(NameNode),后续 `*=` 拆分重包装时避免嵌套 TypeAnnotationNode。 + const targetExpr = leftExpr; annotationExpr = this._parseTypeAnnotation(); - leftExpr = TypeAnnotationNode.create(leftExpr, annotationExpr); + leftExpr = TypeAnnotationNode.create(targetExpr, annotationExpr); if ( !this._parseOptions.isStubFile && @@ -4653,7 +4733,31 @@ export class Parser { this._addSyntaxError(LocMessage.varAnnotationIllegal(), annotationExpr); } - if (!this._consumeTokenIfOperator(OperatorType.Assign)) { + // Viper 扩展:`px: T* = &x` 无空格时,词法将 `*` 与 `=` 合并为 `*=`(同理 `**=`)。 + // 此时 `*`/`**` 是指针类型后缀、`=` 是赋值号,需拆开处理: + // 消费 `*=` 作为一层指针(`**=` 两层),并视为已消费赋值号 `=`。 + let isViperStarEqualAssign = false; + if (this._parseOptions.dialect === 'viper') { + const starAssignOperator = this._peekOperatorType(); + if ( + starAssignOperator === OperatorType.MultiplyEqual || + starAssignOperator === OperatorType.PowerEqual + ) { + const starToken = this._getNextToken(); + annotationExpr = this._createUnaryOperationNode(starToken, annotationExpr, OperatorType.Multiply); + if (starAssignOperator === OperatorType.PowerEqual) { + annotationExpr = this._createUnaryOperationNode( + starToken, + annotationExpr, + OperatorType.Multiply + ); + } + leftExpr = TypeAnnotationNode.create(targetExpr, annotationExpr); + isViperStarEqualAssign = true; + } + } + + if (!isViperStarEqualAssign && !this._consumeTokenIfOperator(OperatorType.Assign)) { return leftExpr; } @@ -4839,6 +4943,11 @@ export class Parser { } let result = this._parseTestExpression(/* allowAssignmentExpression */ false); + + // Viper 扩展:C 指针类型后缀 T* / T** / T***(须在还原 _isParsingTypeAnnotation 之前, + // 使 _parseArithmeticTerm/_parseArithmeticFactor 的星号守卫仍生效)。 + result = this._parseViperPointerTypeSuffix(result); + if (isUnpack) { result = UnpackNode.create(startToken, result); } @@ -4849,6 +4958,34 @@ export class Parser { return result; } + // Viper 扩展:解析 C 指针类型后缀 `T*` / `T**` / `T***`。 + // 每个 '*' 对应一层指针;'**' 词法化为单个 Power 算子、等价两层指针。 + // 结果用 UnaryOperationNode(operator=Multiply) 嵌套表示: + // T* → *(T) → CPtr[T] + // T** → *(*T) → CPtr[CPtr[T]] + // 仅 Viper 方言生效(Python 方言中 '*'/'**' 是二元乘法/幂运算)。 + private _parseViperPointerTypeSuffix(baseExpr: ExpressionNode): ExpressionNode { + if (this._parseOptions.dialect !== 'viper') { + return baseExpr; + } + + let result = baseExpr; + while (true) { + const nextOperator = this._peekOperatorType(); + if (nextOperator === OperatorType.Multiply) { + const starToken = this._getNextToken(); + result = this._createUnaryOperationNode(starToken, result, OperatorType.Multiply); + } else if (nextOperator === OperatorType.Power) { + const starToken = this._getNextToken(); + result = this._createUnaryOperationNode(starToken, result, OperatorType.Multiply); + result = this._createUnaryOperationNode(starToken, result, OperatorType.Multiply); + } else { + break; + } + } + return result; + } + private _reportStringTokenErrors( stringToken: StringToken | FStringStartToken, unescapedResult?: StringTokenUtils.UnescapedString diff --git a/packages/vipeye-internal/src/parser/tokenizer.ts b/packages/vipeye-internal/src/parser/tokenizer.ts index 557767c1..f43f5b7f 100644 --- a/packages/vipeye-internal/src/parser/tokenizer.ts +++ b/packages/vipeye-internal/src/parser/tokenizer.ts @@ -60,6 +60,8 @@ const _keywords: Map = new Map([ ['continue', KeywordType.Continue], ['__debug__', KeywordType.Debug], ['def', KeywordType.Def], + // Viper 扩展:define 宏函数关键字(与 def 语义一致,语法完全相同)。 + ['define', KeywordType.Define], ['del', KeywordType.Del], ['elif', KeywordType.Elif], ['else', KeywordType.Else], diff --git a/packages/vipeye-internal/src/parser/tokenizerTypes.ts b/packages/vipeye-internal/src/parser/tokenizerTypes.ts index b7c6f843..608d1cf0 100644 --- a/packages/vipeye-internal/src/parser/tokenizerTypes.ts +++ b/packages/vipeye-internal/src/parser/tokenizerTypes.ts @@ -121,6 +121,8 @@ export const enum KeywordType { Continue, Debug, Def, + // Viper 扩展:define 宏函数关键字(与 def 语义一致,语法完全相同)。 + Define, Del, Elif, Else, diff --git a/packages/vipeye-internal/src/partialStubService.ts b/packages/vipeye-internal/src/partialStubService.ts index 651a59fb..e2c3ab08 100644 --- a/packages/vipeye-internal/src/partialStubService.ts +++ b/packages/vipeye-internal/src/partialStubService.ts @@ -122,7 +122,10 @@ export class PartialStubService implements SupportPartialStubs { this._realFs.mapDirectory( packagePath, partialStubPackagePath, - (u, fs) => u.hasExtension('.vpi') || (fs.existsSync(u) && fs.statSync(u).isDirectory()) + (u, fs) => + u.hasExtension('.vpi') || + u.hasExtension('.vpnci') || + (fs.existsSync(u) && fs.statSync(u).isDirectory()) ) ); } catch { diff --git a/packages/vipeye-internal/src/tests/__define_macro__/main.vp b/packages/vipeye-internal/src/tests/__define_macro__/main.vp new file mode 100644 index 00000000..e2cc0470 --- /dev/null +++ b/packages/vipeye-internal/src/tests/__define_macro__/main.vp @@ -0,0 +1,5 @@ +define A(): + pass + +def B(): + A() diff --git a/packages/vipeye-internal/src/tests/__hover__/hover.vp b/packages/vipeye-internal/src/tests/__hover__/hover.vp new file mode 100644 index 00000000..c19e53b8 --- /dev/null +++ b/packages/vipeye-internal/src/tests/__hover__/hover.vp @@ -0,0 +1,7 @@ +import t, c + +export def add[T](a: t.CInt, b: t.CInt = 10) -> t.CInt: + return a + b + +def use(): + add(1, 2) diff --git a/packages/vipeye-internal/src/tests/__null_zero__/null_zero.vp b/packages/vipeye-internal/src/tests/__null_zero__/null_zero.vp new file mode 100644 index 00000000..6c26b63a --- /dev/null +++ b/packages/vipeye-internal/src/tests/__null_zero__/null_zero.vp @@ -0,0 +1,19 @@ +import t, c + +def takes_ptr(p: t.CInt | t.CPtr) -> t.CInt | t.CPtr: + return p + +def test_zero_cast(): + p: t.CInt | t.CPtr = None + q: t.CInt | t.CPtr = None + p = 0 + p = 1 + x: t.CInt = 0 + p = x + takes_ptr(0) + takes_ptr(1) + addr: t.CUnsignedLong = 0x1234 + p = addr + p = t.CInt(addr, t.CPtr) + takes_ptr(p) + takes_ptr(q) diff --git a/packages/vipeye-internal/src/tests/__ptr_pat__/ptr_pat.vp b/packages/vipeye-internal/src/tests/__ptr_pat__/ptr_pat.vp new file mode 100644 index 00000000..24e35eb4 --- /dev/null +++ b/packages/vipeye-internal/src/tests/__ptr_pat__/ptr_pat.vp @@ -0,0 +1,10 @@ +import t, llvmlite + +def test(rhs_val: llvmlite.Value | t.CPtr) -> t.CInt: + if rhs_val.Ty is None: + return 0 + match rhs_val.Ty: + case llvmlite.LLVMType.Struct(_, _, _): + tmp: llvmlite.Value | t.CPtr = llvmlite.build_alloca(rhs_val, rhs_val.Ty) + return 0 + return 0 diff --git a/packages/vipeye-internal/src/tests/__renum_kind__/renum_kind.vp b/packages/vipeye-internal/src/tests/__renum_kind__/renum_kind.vp new file mode 100644 index 00000000..ae36fb3f --- /dev/null +++ b/packages/vipeye-internal/src/tests/__renum_kind__/renum_kind.vp @@ -0,0 +1,19 @@ +import t, c + +class MyEnum(t.REnum): + class Int: + Bits: t.CInt + + class Ptr: + Pointee: MyEnum | t.CPtr + +def test_enum(e: MyEnum | t.CPtr) -> int: + k: t.CInt = e.kind() + k2: t.CInt = e.kind + return k + k2 + +def test_assign() -> int: + e: MyEnum | t.CPtr = MyEnum.Int(32) + if e.kind() == 1: + return 1 + return 0 diff --git a/packages/vipeye-internal/src/tests/__ty_narrow_dump.txt b/packages/vipeye-internal/src/tests/__ty_narrow_dump.txt new file mode 100644 index 00000000..5c64c3f1 --- /dev/null +++ b/packages/vipeye-internal/src/tests/__ty_narrow_dump.txt @@ -0,0 +1,42 @@ + L798 .coerce_to_type: (builder: CPtr[IRBuilder], val: CPtr[Value], target_ty: CPtr[LLVMType], node: CPtr[AST] = None, trans: CPtr[Translator] = None) -> CPtr[Value] + L798 .ExprGenBase: CPtr[ExprGenBase] + L798 .Gen: CPtr[Generator] + L801 .fatal_error: (node: CPtr[AST], msg: str, err_type: str = "SyntaxError", col_start: int = 0, col_end: int = 0) -> None + L801 .TypeGenErr: CPtr[TypeGenErr] + L801 .Gen: CPtr[Generator] + L802 .build_store: (builder: CPtr[IRBuilder], val: CPtr[Value], ptr: CPtr[Value]) -> None + L807 .Subscript: Literal[ASTKind.Subscript] + L807 .ASTKind: type[ASTKind] + L808 .MemBuddy: type[MemBuddy] + L808 .CPtr: type[CPtr[Unknown]] + L808 .Pool: CPtr[MemBuddy] + L809 .IRBuilder: type[IRBuilder] + L809 .CPtr: type[CPtr[Unknown]] + L809 ._cur_builder: CPtr[IRBuilder] + L810 .LLVMModule: type[LLVMModule] + L810 .CPtr: type[CPtr[Unknown]] + L810 .Module: CPtr[LLVMModule] + L813 .Value: type[Value] + L813 .CPtr: type[CPtr[Unknown]] + L813 .get_subscript_ptr: (builder: CPtr[IRBuilder], pool: CPtr[MemBuddy], mod: CPtr[LLVMModule], node: CPtr[AST], trans: CPtr[Translator] = None) -> CPtr[Value] + L813 .ExprGenName: CPtr[ExprGenName] + L813 .Gen: CPtr[Generator] + L817 .fatal_error: (node: CPtr[AST], msg: str, err_type: str = "SyntaxError", col_start: int = 0, col_end: int = 0) -> None + L817 .TypeGenErr: CPtr[TypeGenErr] + L817 .Gen: CPtr[Generator] + L820 .LLVMType: type[LLVMType] + L820 .CPtr: type[CPtr[Unknown]] + L821 .Ty: CPtr[LLVMType] + L821 .Pointee: CPtr[LLVMType] + L821 .Ty: CPtr[LLVMType] + L822 .Pointee: CPtr[LLVMType] + L822 .Ty: CPtr[LLVMType] + L825 .fatal_error: (node: CPtr[AST], msg: str, err_type: str = "SyntaxError", col_start: int = 0, col_end: int = 0) -> None + L825 .TypeGenErr: CPtr[TypeGenErr] + L825 .Gen: CPtr[Generator] + L826 .Value: type[Value] + L826 .CPtr: type[CPtr[Unknown]] + L826 .build_load: (builder: CPtr[IRBuilder], ty: CPtr[LLVMType], ptr: CPtr[Value]) -> CPtr[Value] + L830 .fatal_error: (node: CPtr[AST], msg: str, err_type: str = "SyntaxError", col_start: int = 0, col_end: int = 0) -> None + L830 .TypeGenErr: CPtr[TypeGenErr] + L830 .Gen: CPtr[Generator] diff --git a/packages/vipeye-internal/src/tests/__unary_ptr__/unary_ptr.vp b/packages/vipeye-internal/src/tests/__unary_ptr__/unary_ptr.vp new file mode 100644 index 00000000..bc3d0438 --- /dev/null +++ b/packages/vipeye-internal/src/tests/__unary_ptr__/unary_ptr.vp @@ -0,0 +1,10 @@ +import t, c + +def main() -> int: + x: int = 42 + px: int | t.CPtr = &x + *px = 100 + y: int = *px + pz: int* = &x + *pz = 7 + return 0 diff --git a/packages/vipeye-internal/src/tests/viperBuiltinsVpTypedef.test.ts b/packages/vipeye-internal/src/tests/viperBuiltinsVpTypedef.test.ts new file mode 100644 index 00000000..c4790d56 --- /dev/null +++ b/packages/vipeye-internal/src/tests/viperBuiltinsVpTypedef.test.ts @@ -0,0 +1,219 @@ +/* + * viperBuiltinsVpTypedef.test.ts + * + * 验证 Viper 方言下注解名解析对 includes/builtins.vp(真实源码,非 .vpi 存根) + * 中 t.CTypedef 类型别名的兜底搜索("builtins 最后匹配",对齐 TransPyC + * TypeAnnotationResolver / TPV _lookup_builtins_typedef): + * - `rowptr[int]` 注解 → rowptr 为 builtins.vp 中的 typedef,可解析为 CPtr[int] + * - `size_t` 注解 → 解析为 t.CSizeT + * - `int8` 注解 → 解析为 t.CInt8T + * - 均无需显式 import builtins(builtins 不在用户文件 import 闭包内) + * - 不报"未定义 rowptr/size_t/int8" + * + * 运行:npx jest src/tests/viperBuiltinsVpTypedef.test.ts --runInBand + */ + +import assert from 'assert'; +import * as path from 'path'; + +import { ImportResolver } from '../analyzer/importResolver'; +import { Program } from '../analyzer/program'; +import { ParseTreeWalker } from '../analyzer/parseTreeWalker'; +import { TypeEvaluator } from '../analyzer/typeEvaluatorTypes'; +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 { FunctionNode } from '../parser/parseNodes'; + +(global as any).__rootDirectory = path.resolve(); + +const TRANSPYC_INCLUDES = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes'; + +function buildProgram(source: string) { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const fs = require('fs'); + const testRoot = path.join(__dirname, '__builtins_vp_typedef__'); + fs.rmSync(testRoot, { recursive: true, force: true }); + fs.mkdirSync(testRoot, { recursive: true }); + const filePath = path.join(testRoot, 'main.vp'); + fs.writeFileSync(filePath, source, '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, + [UriEx.file(TRANSPYC_INCLUDES)], + /* 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()) { + // 循环 + } + + const sourceFile = program.getSourceFile(uri); + const parseResults = sourceFile!.getParseResults(); + return { program, configOptions, parseResults, uri, serviceProvider }; +} + +// 收集函数参数注解类型(打印名,expandTypeAlias 展开别名) +function collectParamAnnotationTypes( + program: Program, + parseResults: any +): Map { + const evaluator = program.evaluator as TypeEvaluator | undefined; + assert(evaluator); + const ev = evaluator; + const result = new Map(); + + class ParamCollector extends ParseTreeWalker { + override visitFunction(node: FunctionNode) { + const funcName = node.d.name.d.value; + for (const param of node.d.params) { + const ann = (param as any).d?.annotation; + if (ann) { + const t = ev.getType(ann as any); + const list = result.get(funcName) ?? []; + list.push(t ? program.printType(t, { expandTypeAlias: true }) : '(undefined)'); + result.set(funcName, list); + } + } + return true; + } + } + new ParamCollector().walk(parseResults.parserOutput.parseTree); + + return result; +} + +test('builtins.vp(includes 版本)typedef 在注解中可直接解析', () => { + const source = `import t + +def f(x: rowptr[int]) -> int: + return 0 + +def g() -> size_t: + return 0 + +def h(a: int8) -> int: + return 0 +`; + const { program, configOptions, parseResults, uri, serviceProvider } = buildProgram(source); + + // 1. 无未定义诊断 + const diagnostics = (program.getSourceFile(uri)?.getDiagnostics(configOptions) || []).map( + (d) => `[category=${d.category}] ${d.message}` + ); + const undefinedDiags = diagnostics.filter((d) => d.includes('未定义') || d.includes('undefined')); + assert.strictEqual( + undefinedDiags.length, + 0, + `builtins.vp typedef 不应报未定义,实际: ${JSON.stringify(undefinedDiags)}` + ); + + // 2. 参数注解类型解析:rowptr[int] → CPtr[int] + const types = collectParamAnnotationTypes(program, parseResults!); + const fTypes = types.get('f'); + assert(fTypes && fTypes.some((t) => t.includes('CPtr') && t.includes('int')), `f(x) 应为 CPtr[int],实际: ${JSON.stringify(fTypes)}`); + + // 3. 返回注解 size_t → CSizeT + const gReturn = (() => { + const evaluator = program.evaluator as TypeEvaluator | undefined; + assert(evaluator); + const ev = evaluator; + let retType: string | undefined; + class RetCollector extends ParseTreeWalker { + override visitFunction(node: FunctionNode) { + if (node.d.name.d.value === 'g' && node.d.returnAnnotation) { + const t = ev.getType(node.d.returnAnnotation as any); + retType = t ? program.printType(t, { expandTypeAlias: true }) : '(undefined)'; + } + return true; + } + } + new RetCollector().walk(parseResults!.parserOutput.parseTree); + return retType; + })(); + assert( + gReturn && (gReturn === 'CSizeT' || gReturn.includes('CSizeT')), + `g() 返回注解 size_t 应为 CSizeT,实际: ${gReturn}` + ); + + // 4. 参数注解 int8 → CInt8T + const hTypes = types.get('h'); + assert(hTypes && hTypes.some((t) => t.includes('CInt8T')), `h(a) 应为 CInt8T,实际: ${JSON.stringify(hTypes)}`); + + program.dispose(); + serviceProvider.dispose(); + require('fs').rmSync(path.join(__dirname, '__builtins_vp_typedef__'), { recursive: true, force: true }); +}); + +test('builtins.vp typedef 与 builtins.vpi 内建并存(int/str 等不被破坏)', () => { + const source = `import t + +def f(x: rowptr[int]) -> str: + s: str = "ok" + return s +`; + const { program, configOptions, uri, serviceProvider } = buildProgram(source); + + const diagnostics = (program.getSourceFile(uri)?.getDiagnostics(configOptions) || []).map( + (d) => `[category=${d.category}] ${d.message}` + ); + const errors = diagnostics.filter((d) => d.startsWith('[category=0]') || d.startsWith('[category=1]')); + assert.strictEqual( + errors.length, + 0, + `builtins.vpi 内建(str 等)应与 builtins.vp typedef 并存,实际: ${JSON.stringify(diagnostics)}` + ); + + program.dispose(); + serviceProvider.dispose(); + require('fs').rmSync(path.join(__dirname, '__builtins_vp_typedef__'), { recursive: true, force: true }); +}); + +test('builtins.vp 同名声明优先覆盖 builtins.vpi(ord 参数为 t.CChar)', () => { + const source = `import t + +def f() -> int: + c: t.CChar = ord("a") + return c +`; + const { program, parseResults, serviceProvider } = buildProgram(source); + + // ord 在 builtins.vp 中声明为 ord(s: t.CChar) -> int, + // 覆盖 builtins.vpi 的 ord(c: str | bytes | bytearray, /) -> int。 + // 通过 main 文件作用域链(含 builtin 作用域)解析 ord 符号。 + const AnalyzerNodeInfo = require('../analyzer/analyzerNodeInfo'); + const mainScope = AnalyzerNodeInfo.getScope(parseResults!.parserOutput.parseTree); + const ordSymbol = mainScope?.lookUpSymbolRecursive?.('ord')?.symbol; + assert(ordSymbol, 'ord 符号未找到(builtins.vp 覆盖未生效)'); + + const ordType = program.getTypeOfSymbol(ordSymbol); + const ordSignature = program.printType(ordType, { expandTypeAlias: true }); + assert( + ordSignature.includes('CChar'), + `ord 应使用 builtins.vp 的 t.CChar 签名,实际: ${ordSignature}` + ); + + program.dispose(); + serviceProvider.dispose(); + require('fs').rmSync(path.join(__dirname, '__builtins_vp_typedef__'), { recursive: true, force: true }); +}); diff --git a/packages/vipeye-internal/src/tests/viperDefineMacro.test.ts b/packages/vipeye-internal/src/tests/viperDefineMacro.test.ts new file mode 100644 index 00000000..e1801782 --- /dev/null +++ b/packages/vipeye-internal/src/tests/viperDefineMacro.test.ts @@ -0,0 +1,179 @@ +/* + * viperDefineMacro.test.ts + * + * 验证 Viper define 宏函数关键字(语法与 def 完全一致,语义为编译期宏展开): + * - `define square(x) -> int` 解析为 FunctionNode 且 isDefine=true(普通 def 为 false) + * - 宏函数名在定义处与调用处显示为 macro(深蓝色);普通 def 函数名仍为 function + * - 宏函数与普通函数一样参与类型分析(无诊断错误) + * + * 运行:npx jest src/tests/viperDefineMacro.test.ts --runInBand + */ + +import assert from 'assert'; +import * as path from 'path'; + +import { ImportResolver } from '../analyzer/importResolver'; +import { Program } from '../analyzer/program'; +import { ParseTreeWalker } from '../analyzer/parseTreeWalker'; +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 { FunctionNode } from '../parser/parseNodes'; +import { SemanticTokenTypes } from '../languageService/semanticTokenTypes'; + +(global as any).__rootDirectory = path.resolve(); + +function buildProgram(files: Record) { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const fs = require('fs'); + const testRoot = path.join(__dirname, '__define_macro__'); + fs.rmSync(testRoot, { recursive: true, force: true }); + for (const [relPath, content] of Object.entries(files)) { + const absPath = path.join(testRoot, relPath); + fs.mkdirSync(path.dirname(absPath), { recursive: true }); + fs.writeFileSync(absPath, content, '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 uris = Object.keys(files).map((relPath) => UriEx.file(path.join(testRoot, relPath))); + const urisMap: Record = {}; + for (const relPath of Object.keys(files)) { + urisMap[relPath] = UriEx.file(path.join(testRoot, relPath)); + } + program.setTrackedFiles(uris); + while (program.analyze()) { + // 循环 + } + + return { program, configOptions, urisMap, testRoot, serviceProvider }; +} + +function getDiagnostics(program: Program, configOptions: ConfigOptions, uri: any): string[] { + const sourceFile = program.getSourceFile(uri); + assert(sourceFile, 'source file not found'); + return (sourceFile.getDiagnostics(configOptions) || []).map((diag) => { + const line = diag.range ? diag.range.start.line + 1 : -1; + return `[category=${diag.category}][line=${line}] ${diag.message}`; + }); +} + +/** + * 语义 token 辅助:解码 SemanticTokens 相对编码,返回 line 级 token 映射。 + * 返回 Map> + */ +function getSemanticTokens(program: Program, uri: any): Map { + const { SemanticTokenProvider } = require('../languageService/semanticTokenProvider'); + const { LogTracker } = require('../common/logTracker'); + const provider = new SemanticTokenProvider(new LogTracker()); + const result = provider.provideFull(program, uri); + assert(result, 'no semantic tokens produced'); + + const sourceFile = program.getSourceFile(uri); + const lines = sourceFile!.getParseResults()!.text.split('\n'); + + const map = new Map(); + let cursorLine = 0; + let cursorChar = 0; + for (let i = 0; i < result.data.length; i += 5) { + const deltaLine = result.data[i]; + const deltaStart = result.data[i + 1]; + const length = result.data[i + 2]; + const tokenType = result.data[i + 3]; + const tokenMod = result.data[i + 4]; + cursorLine += deltaLine; + cursorChar = deltaLine === 0 ? cursorChar + deltaStart : deltaStart; + const lineText = lines[cursorLine] || ''; + const text = lineText.substring(cursorChar, cursorChar + length); + if (!map.has(cursorLine)) { + map.set(cursorLine, []); + } + map.get(cursorLine)!.push({ start: cursorChar, text, tokenType, tokenModifiers: tokenMod }); + } + return map; +} + +test('define 宏函数:解析为 FunctionNode 且 isDefine=true,普通 def 为 false,无诊断错误', () => { + const { program, configOptions, urisMap } = buildProgram({ + 'main.vp': `define A(): + pass + +def B(): + A() +`, + }); + + // 1. 无诊断错误(宏函数与普通函数一样参与类型分析) + const mainDiags = getDiagnostics(program, configOptions, urisMap['main.vp']); + assert( + mainDiags.length === 0, + `main.vp 不应有诊断,实际: ${JSON.stringify(mainDiags)}` + ); + + // 2. AST:A 是 define 宏函数(isDefine=true),B 是普通函数(isDefine=false) + const sourceFile = program.getSourceFile(urisMap['main.vp']); + const parseResults = sourceFile!.getParseResults(); + assert(parseResults, 'parse results not found'); + + const isDefineMap = new Map(); + class FunctionIsDefineWalker extends ParseTreeWalker { + override visitFunction(node: FunctionNode): boolean { + isDefineMap.set(node.d.name.d.value, node.d.isDefine); + return true; + } + } + new FunctionIsDefineWalker().walk(parseResults.parserOutput.parseTree); + assert(isDefineMap.get('A') === true, 'A 应标记为 define 宏函数'); + assert(isDefineMap.get('B') === false, 'B 应为普通函数(isDefine=false)'); +}); + +test('define 宏函数:定义处与调用处的函数名显示为 macro,普通 def 仍为 function', () => { + const { program, urisMap } = buildProgram({ + 'main.vp': `define A(): + pass + +def B(): + A() +`, + }); + + const tokens = getSemanticTokens(program, urisMap['main.vp']); + const lineTokens = (line: number) => tokens.get(line) || []; + + // 第 0 行 `define A():`:A 定义处 → macro + const defineTokens = lineTokens(0).filter((t) => t.text === 'A'); + assert(defineTokens.length === 1, '应找到 A 定义处语义 token'); + assert(defineTokens[0].tokenType === SemanticTokenTypes.macro, 'A 定义处应为 macro'); + + // 第 4 行 ` A()`:A 调用处 → macro + const callTokens = lineTokens(4).filter((t) => t.text === 'A'); + assert(callTokens.length === 1, '应找到 A 调用处语义 token'); + assert(callTokens[0].tokenType === SemanticTokenTypes.macro, 'A 调用处应为 macro'); + + // 第 3 行 `def B():`:B 定义处 → function(普通函数不受影响) + const bDef = lineTokens(3).find((t) => t.text === 'B'); + assert(bDef, '应找到 B 定义处语义 token'); + assert(bDef.tokenType === SemanticTokenTypes.function, 'B 定义处应为 function'); +}); diff --git a/packages/vipeye-internal/src/tests/viperEnumerate.test.ts b/packages/vipeye-internal/src/tests/viperEnumerate.test.ts new file mode 100644 index 00000000..b10796f9 --- /dev/null +++ b/packages/vipeye-internal/src/tests/viperEnumerate.test.ts @@ -0,0 +1,161 @@ +/* + * viperEnumerate.test.ts + * + * 验证 Viper 方言下内置 enumerate 的语义: + * - enumerate(str) 调用类型为 enumerate[t.CChar](_T 按 for 原迭代逻辑绑定元素类型) + * - `for i, ch in enumerate(str)` 中 i 为 t.CSizeT(size_t 索引)、ch 为迭代元素 t.CChar + * - 与 Python 声明(Iterable[_T] 协议、tuple[int, _T])的差异由 Viper 特判覆盖 + * + * 运行:npx jest src/tests/viperEnumerate.test.ts --runInBand + */ + +import assert from 'assert'; +import * as path from 'path'; + +import { ImportResolver } from '../analyzer/importResolver'; +import { Program } from '../analyzer/program'; +import { ParseTreeWalker } from '../analyzer/parseTreeWalker'; +import { TypeEvaluator } from '../analyzer/typeEvaluatorTypes'; +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 { ForNode, CallNode, ExpressionNode, ParseNodeType } from '../parser/parseNodes'; + +(global as any).__rootDirectory = path.resolve(); + +function buildProgram(source: string) { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const fs = require('fs'); + const testRoot = path.join(__dirname, '__enumerate__'); + fs.rmSync(testRoot, { recursive: true, force: true }); + fs.mkdirSync(testRoot, { recursive: true }); + const filePath = path.join(testRoot, 'main.vp'); + fs.writeFileSync(filePath, source, '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()) { + // 循环 + } + + const sourceFile = program.getSourceFile(uri); + const parseResults = sourceFile!.getParseResults(); + return { program, configOptions, parseResults, uri, serviceProvider }; +} + +// 收集所有 for 循环:iterable 表达式类型 + target 解包变量类型 +function collectForTargets( + program: Program, + parseResults: any +): { iterableTypes: string[]; targets: Map } { + const evaluator = program.evaluator as TypeEvaluator | undefined; + assert(evaluator); + const ev = evaluator; + const iterableTypes: string[] = []; + const targets = new Map(); + + class ForCollector extends ParseTreeWalker { + override visitFor(node: ForNode) { + const iterableType = ev.getType(node.d.iterableExpr as any); + if (iterableType) { + iterableTypes.push(program.printType(iterableType)); + } + const targetExpr = node.d.targetExpr; + if (targetExpr.nodeType === ParseNodeType.Tuple) { + for (const item of (targetExpr as any).d.items as ExpressionNode[]) { + if (item.nodeType === ParseNodeType.Name) { + const name = (item as any).d.value as string; + const t = ev.getType(item as any); + const list = targets.get(name) ?? []; + list.push(t ? program.printType(t) : '(undefined)'); + targets.set(name, list); + } + } + } + return true; + } + } + new ForCollector().walk(parseResults.parserOutput.parseTree); + + return { iterableTypes, targets }; +} + +test('for i, ch in enumerate(kw_arg):i 为 t.CSizeT、ch 为 t.CChar', () => { + const source = `import t + +def f(kw_arg: str) -> int: + for i, ch in enumerate(kw_arg): + pass + return 0 +`; + const { program, parseResults, serviceProvider } = buildProgram(source); + + const { iterableTypes, targets } = collectForTargets(program, parseResults!); + + // 1. enumerate 调用类型:元素类型按 for 原迭代逻辑(str → t.CChar) + assert(iterableTypes.some((t) => t === 'enumerate[CChar]'), `实际: ${JSON.stringify(iterableTypes)}`); + + // 2. 索引 i 为 t.CSizeT(size_t) + const iTypes = targets.get('i'); + assert(iTypes && iTypes.some((t) => t === 'CSizeT'), `i 应为 CSizeT,实际: ${JSON.stringify(iTypes)}`); + + // 3. 元素 ch 为 t.CChar + const chTypes = targets.get('ch'); + assert(chTypes && chTypes.some((t) => t === 'CChar'), `ch 应为 CChar,实际: ${JSON.stringify(chTypes)}`); + + program.dispose(); + serviceProvider.dispose(); + require('fs').rmSync(path.join(__dirname, '__enumerate__'), { recursive: true, force: true }); +}); + +test('enumerate 迭代 CPtr 元素时 ch 为元素类型 T', () => { + const source = `import t + +def f(items: t.CPtr[int]) -> int: + for i, item in enumerate(items): + pass + return i +`; + const { program, parseResults, serviceProvider } = buildProgram(source); + + const { iterableTypes, targets } = collectForTargets(program, parseResults!); + + // 1. CPtr[int] 迭代 → 元素类型 int,enumerate 特化为 enumerate[int] + assert(iterableTypes.some((t) => t === 'enumerate[int]'), `实际: ${JSON.stringify(iterableTypes)}`); + + // 2. 索引 i 仍为 t.CSizeT + const iTypes = targets.get('i'); + assert(iTypes && iTypes.some((t) => t === 'CSizeT'), `i 应为 CSizeT,实际: ${JSON.stringify(iTypes)}`); + + // 3. 元素 item 为 int + const itemTypes = targets.get('item'); + assert(itemTypes && itemTypes.some((t) => t === 'int'), `item 应为 int,实际: ${JSON.stringify(itemTypes)}`); + + program.dispose(); + serviceProvider.dispose(); + require('fs').rmSync(path.join(__dirname, '__enumerate__'), { recursive: true, force: true }); +}); diff --git a/packages/vipeye-internal/src/tests/viperEnumerateUnbound.test.ts b/packages/vipeye-internal/src/tests/viperEnumerateUnbound.test.ts new file mode 100644 index 00000000..c26e1f7d --- /dev/null +++ b/packages/vipeye-internal/src/tests/viperEnumerateUnbound.test.ts @@ -0,0 +1,123 @@ +/* + * viperForLoopVarAfter.test.ts(文件:viperEnumerateUnbound.test.ts) + * + * 验证 Viper 方言下 for 循环变量在循环后的语义: + * - `for i, ch in enumerate(kw_arg)` 后,i 保留迭代类型 t.CSizeT(C 风格), + * 而非 Python 语义的 `Unbound | CSizeT`(循环可能零次执行) + * - 循环后 `param_name[start + i + 1]` 的加法不再报"未绑定/加法违法" + * + * 运行:npx jest src/tests/viperEnumerateUnbound.test.ts --runInBand + */ + +import assert from 'assert'; +import * as path from 'path'; + +import { ImportResolver } from '../analyzer/importResolver'; +import { Program } from '../analyzer/program'; +import { ParseTreeWalker } from '../analyzer/parseTreeWalker'; +import { TypeEvaluator } from '../analyzer/typeEvaluatorTypes'; +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 { convertOffsetToPosition } from '../common/positionUtils'; +import { BinaryOperationNode, ForNode } from '../parser/parseNodes'; + +(global as any).__rootDirectory = path.resolve(); + +function buildProgram(source: string) { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const fs = require('fs'); + const testRoot = path.join(__dirname, '__enumerate_unbound__'); + fs.rmSync(testRoot, { recursive: true, force: true }); + fs.mkdirSync(testRoot, { recursive: true }); + const filePath = path.join(testRoot, 'main.vp'); + fs.writeFileSync(filePath, source, '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()) { + // 循环 + } + + const sourceFile = program.getSourceFile(uri); + const parseResults = sourceFile!.getParseResults(); + return { program, configOptions, parseResults, uri, serviceProvider }; +} + +test('for enumerate 后 i 的类型与诊断 dump', () => { + const source = `import t + +def f(kw_arg: str, param_name: str) -> int: + start: t.CSizeT = 0 + if param_name[0] == '%': + start = 1 + for i, ch in enumerate(kw_arg): + c: t.CChar = param_name[start + i] + if c == '\\0' or c != ch: + return 0 + if param_name[start + i + 1] != '\\0': + return 0 + return 1 +`; + const { program, configOptions, parseResults, uri, serviceProvider } = buildProgram(source); + + const evaluator = program.evaluator as TypeEvaluator | undefined; + assert(evaluator); + assert(parseResults); + const ev = evaluator; + const lines = parseResults.parserOutput.lines; + const out: string[] = []; + const diags = (program.getSourceFile(uri)!.getDiagnostics(configOptions) || []).map((d) => d.message); + + class BinOpCollector extends ParseTreeWalker { + override visitBinaryOperation(node: BinaryOperationNode) { + const pos = convertOffsetToPosition(node.start, lines); + const t = ev.getType(node as any); + out.push( + ` L${pos.line + 1} binop=${node.d.operator} type=${t ? program.printType(t) : '(undefined)'}` + ); + return true; + } + } + new BinOpCollector().walk(parseResults.parserOutput.parseTree); + + // Viper(C 风格):for 循环变量在循环后仍视为已定义(保留迭代类型), + // 循环体内与循环后的 start + i / start + i + 1 均应为纯 CSizeT(无 Unbound)。 + const addOps = out.filter((l) => l.includes('binop=0')); + assert(addOps.length >= 3, `应存在 3 个加法运算(循环体内 1 + 循环后 2),实际: ${JSON.stringify(out)}`); + assert( + addOps.every((l) => l.includes('type=CSizeT') && !l.includes('Unbound') && !l.includes('Unknown')), + `加法运算应均为纯 CSizeT,实际: ${JSON.stringify(addOps)}` + ); + + // 不应有诊断(循环后使用 i 不再报未绑定/加法违法) + assert(diags.length === 0, `不应有诊断,实际: ${JSON.stringify(diags)}`); + + program.dispose(); + serviceProvider.dispose(); + require('fs').rmSync(path.join(__dirname, '__enumerate_unbound__'), { recursive: true, force: true }); +}); diff --git a/packages/vipeye-internal/src/tests/viperFunctionHover.test.ts b/packages/vipeye-internal/src/tests/viperFunctionHover.test.ts new file mode 100644 index 00000000..990aa75c --- /dev/null +++ b/packages/vipeye-internal/src/tests/viperFunctionHover.test.ts @@ -0,0 +1,113 @@ +/* + * viperFunctionHover.test.ts + * + * 验证 Viper 方言下 hover 函数时显示详尽调试信息: + * - 定义位置(文件:行:列) + * - 修饰符(extern/export/inline) + * - 泛型参数 + * - 参数列表(名称、类型、默认值、*args/**kwargs 类别) + * - 返回类型 + * + * 运行:npx jest src/tests/viperFunctionHover.test.ts --runInBand + */ + +import assert from 'assert'; +import * as path from 'path'; +import { CancellationToken, MarkupContent, MarkupKind, Position } from 'vscode-languageserver'; + +import { ImportResolver } from '../analyzer/importResolver'; +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 { HoverProvider } from '../languageService/hoverProvider'; + +(global as any).__rootDirectory = path.resolve(); + +const TRANSPYC_INCLUDES = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes'; + +function buildProgramForCode(code: string) { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const projectRoot = UriEx.file(TRANSPYC_INCLUDES); + 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 fs = require('fs'); + const testDir = path.join(__dirname, '__hover__'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'hover.vp'); + fs.writeFileSync(testFile, code, 'utf8'); + + const uri = UriEx.file(testFile); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + return { program, configOptions, uri, testDir, serviceProvider }; +} + +test('Viper hover 函数显示详尽调试信息', () => { + const code = `import t, c + +export def add[T](a: t.CInt, b: t.CInt = 10) -> t.CInt: + return a + b + +def use(): + add(1, 2) +`; + const { program, uri, serviceProvider } = buildProgramForCode(code); + + // 悬停在调用点 `add(1, 2)` 的 `add` 上(L7,零基行 6,列 4) + const position: Position = { line: 6, character: 4 }; + const provider = new HoverProvider(program as any, uri, position, MarkupKind.Markdown, CancellationToken.None); + const hover = provider.getHover(); + assert(hover, 'hover result should not be null'); + const contents = (hover.contents as MarkupContent).value; + console.log('=== hover 内容 ==='); + console.log(contents); + + // 签名部分(原有 python 代码块) + assert(contents.includes('(function)'), `should include signature: ${contents}`); + // 调试信息标题 + assert(contents.includes('函数调试信息'), `should include debug heading: ${contents}`); + // 定义位置(此文件:3:17 —— `add` 在 L3 第 17 列) + assert(contents.includes('位置'), `should include 位置: ${contents}`); + assert(contents.includes('此文件'), `should include 此文件: ${contents}`); + // 修饰符 + assert(contents.includes('修饰符'), `should include 修饰符: ${contents}`); + assert(contents.includes('export'), `should include export modifier: ${contents}`); + // 泛型参数 + assert(contents.includes('泛型参数'), `should include 泛型参数: ${contents}`); + assert(contents.includes('[T]'), `should include [T]: ${contents}`); + // 参数 + assert(contents.includes('参数'), `should include 参数: ${contents}`); + assert(contents.includes('a: CInt'), `should include param a: ${contents}`); + assert(contents.includes('b: CInt = 10'), `should include param b with default: ${contents}`); + // 返回类型 + assert(contents.includes('返回类型'), `should include 返回类型: ${contents}`); + assert(contents.includes('CInt'), `should include return type: ${contents}`); + + program.dispose(); + serviceProvider.dispose(); +}); diff --git a/packages/vipeye-internal/src/tests/viperGenericTypeVarCall.test.ts b/packages/vipeye-internal/src/tests/viperGenericTypeVarCall.test.ts index 887ca608..f79f243e 100644 --- a/packages/vipeye-internal/src/tests/viperGenericTypeVarCall.test.ts +++ b/packages/vipeye-internal/src/tests/viperGenericTypeVarCall.test.ts @@ -1,946 +1,1194 @@ -/* - * viperGenericTypeVarCall.test.ts - * - * 验证 Viper 泛型类方法中 `T(v)`(用类型变量做 C 风格强转)不应报 - * "Object of type TypeVar is not callable" / "Attribute __call__ is unknown"。 - * - * 场景:TransPyC/Test/GenericTest/App/main.vp - * class Box[T](): - * value: T - * def __init__(self, v: T): - * self.value = T(v) # ← T(v) 类型变量调用 - * - * 运行:npx jest src/tests/viperGenericTypeVarCall.test.ts --runInBand - */ - -import assert from 'assert'; -import * as path from 'path'; - -import { ImportResolver } from '../analyzer/importResolver'; -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'; - -// 允许 vipershed-fallback(t.vpi/c.vpi 存根)被定位。 -(global as any).__rootDirectory = path.resolve(); - -const TRANSPYC_INCLUDES = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes'; -const GENERIC_TEST = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\Test\\GenericTest\\App\\main.vp'; - -function buildProgram(targetPath: string) { - const tempFile = new RealTempFile(); - const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); - const projectRoot = UriEx.file(path.dirname(targetPath)); - 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(targetPath); - program.setTrackedFiles([uri]); - while (program.analyze()) { - // 循环直到分析完成 - } - - return { program, serviceProvider, execEnv, uri, configOptions }; -} - -function getDiagnostics(program: Program, configOptions: ConfigOptions, uri: any): string[] { - const sourceFile = program.getSourceFile(uri); - assert(sourceFile, 'source file not found'); - return (sourceFile.getDiagnostics(configOptions) || []).map( - (diag) => `[category=${diag.category}] ${diag.message}` - ); -} - -test('GenericTest 泛型类 Box 中 T(v) 类型变量调用不报 not callable', () => { - const { program, serviceProvider, uri, configOptions } = buildProgram(GENERIC_TEST); - - const diagnostics = getDiagnostics(program, configOptions, uri); - console.log('=== GenericTest not-callable diagnostics ==='); - const notCallable = diagnostics.filter( - (d) => d.includes('not callable') || d.includes('not callable') || d.includes('不可调用') - ); - notCallable.forEach((d) => console.log(` ${d}`)); - - assert.strictEqual( - notCallable.length, - 0, - `unexpected not-callable errors: ${JSON.stringify(notCallable)}` - ); - - program.dispose(); - serviceProvider.dispose(); -}); - -test('最小复现:泛型类方法中 T(v) 类型变量调用不报 not callable', () => { - const tempFile = new RealTempFile(); - const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); - const projectRoot = UriEx.file(TRANSPYC_INCLUDES); - 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 code = `import t - -class Box[T](): - value: T - - def __init__(self, v: T): - self.value = T(v) - - def get(self) -> T: - return self.value - -def make_box(): - b = Box(42) - return b.get() -`; - const testDir = path.join(__dirname, '__typevar_call_test__'); - const fs = require('fs'); - fs.mkdirSync(testDir, { recursive: true }); - const testFile = path.join(testDir, 'typevar.vp'); - fs.writeFileSync(testFile, code, 'utf8'); - - try { - const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); - const program = new Program(importResolver, configOptions, serviceProvider); - const uri = UriEx.file(testFile); - program.setTrackedFiles([uri]); - while (program.analyze()) { - // 循环 - } - - const diagnostics = getDiagnostics(program, configOptions, uri); - console.log('=== typevar.vp diagnostics ==='); - diagnostics.forEach((d) => console.log(` ${d}`)); - - const notCallable = diagnostics.filter( - (d) => d.includes('not callable') || d.includes('不可调用') || d.includes('__call__') - ); - assert.strictEqual( - notCallable.length, - 0, - `unexpected not-callable errors: ${JSON.stringify(diagnostics)}` - ); - - program.dispose(); - } finally { - fs.rmSync(testDir, { recursive: true, force: true }); - } - serviceProvider.dispose(); -}); - - - -test('PEP 695 显式类型实参 double[t.CInt](21) 不报不可下标', () => { - const tempFile = new RealTempFile(); - const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); - const projectRoot = UriEx.file(TRANSPYC_INCLUDES); - 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 code = `import t - -def double[T](x: int) -> int: - return x * 2 - -def test_explicit_type_arg(): - r: t.CInt = double[t.CInt](21) -`; - const testDir = path.join(__dirname, '__typevar_call_test__'); - const fs = require('fs'); - fs.mkdirSync(testDir, { recursive: true }); - const testFile = path.join(testDir, 'explicit_type_arg.vp'); - fs.writeFileSync(testFile, code, 'utf8'); - - try { - const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); - const program = new Program(importResolver, configOptions, serviceProvider); - const uri = UriEx.file(testFile); - program.setTrackedFiles([uri]); - while (program.analyze()) { - // 循环 - } - - const diagnostics = getDiagnostics(program, configOptions, uri); - console.log('=== explicit_type_arg.vp diagnostics ==='); - diagnostics.forEach((d) => console.log(` ${d}`)); - - const notSubscriptable = diagnostics.filter( - (d) => d.includes('not subscriptable') || d.includes('不可下标') - ); - assert.strictEqual( - notSubscriptable.length, - 0, - `unexpected subscriptable errors: ${JSON.stringify(diagnostics)}` - ); - - program.dispose(); - } finally { - fs.rmSync(testDir, { recursive: true, force: true }); - } - serviceProvider.dispose(); -}); - - -test('c.Deref 对 IntPair | t.CPtr 联合返回结构体,成员访问解析为 CInt', () => { - const tempFile = new RealTempFile(); - const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); - const projectRoot = UriEx.file(TRANSPYC_INCLUDES); - 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 code = `import t -import c - -class IntPair: - a: t.CInt - b: t.CInt - -def generic_swap[T](p: IntPair | t.CPtr): - tmp: t.CInt = c.Deref(p).a - c.Deref(p).a = c.Deref(p).b - c.Deref(p).b = tmp - bad: t.CInt = c.Deref(p).zzz -`; - const testDir = path.join(__dirname, '__deref_test__'); - const fs = require('fs'); - fs.mkdirSync(testDir, { recursive: true }); - const testFile = path.join(testDir, 'deref.vp'); - fs.writeFileSync(testFile, code, 'utf8'); - - try { - const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); - const program = new Program(importResolver, configOptions, serviceProvider); - const uri = UriEx.file(testFile); - program.setTrackedFiles([uri]); - while (program.analyze()) { - // 循环 - } - - const diagnostics = getDiagnostics(program, configOptions, uri); - console.log('=== deref.vp diagnostics ==='); - diagnostics.forEach((d) => console.log(` ${d}`)); - - // 期望恰好 1 条"没有属性":c.Deref(p).zzz 应报错(证明 Deref 结果是 - // 具体结构体 IntPair 而非 Unknown——Unknown 上访问任何成员都静默通过), - // 而 c.Deref(p).a / .b 都不应报。 - const noAttr = diagnostics.filter( - (d) => - d.includes('无法访问') || - d.includes('没有属性') || - d.includes('has no attribute') || - d.includes('未知的属性') - ); - assert.strictEqual( - noAttr.length, - 1, - `expected exactly 1 no-attribute error, got: ${JSON.stringify(diagnostics)}` - ); - assert.ok(noAttr[0].includes('zzz'), `expected zzz error: ${JSON.stringify(diagnostics)}`); - - program.dispose(); - } finally { - fs.rmSync(testDir, { recursive: true, force: true }); - } - serviceProvider.dispose(); -}); - - -test('Viper 关键字 inline/export/extern/state/enum/rnum/struct 解析与语义', () => { - const tempFile = new RealTempFile(); - const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); - const projectRoot = UriEx.file(TRANSPYC_INCLUDES); - 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 code = `import t -import c - -# 单独的 struct 关键字(等价 class) -struct IntPair: - a: t.CInt - b: t.CInt - -# 修饰关键字 + def(等价 t.CInline / t.CExport / t.CExtern) -inline def double_inline(x: t.CInt) -> t.CInt: - return x * 2 - -export def double_export(x: t.CInt) -> t.CInt: - return x * 2 - -extern def double_extern(x: t.CInt) -> t.CInt: - pass - -# state 不是关键字,仍可作字段名/成员名 -state: t.CInt = 0 - -# enum / rnum + class(等价 t.CEnum / t.REnum) -enum class Color: - RED: t.CInt = 0 - GREEN: t.CInt = 1 - -rnum class LLVMType: - class Int: - bits: t.CInt - class Ptr: - pointee: t.CPtr - -# enum / rnum 单独使用(等价 class) -enum A: - k: t.State - -rnum B: - class Inner: - x: t.CInt - -# union + class(等价 t.CUnion 风格) -union class Value: - i: t.CInt - f: t.CFloat - -# union 单独使用(等价 class) -union D: - x: t.CInt - -def swap(p: IntPair | t.CPtr): - tmp: t.CInt = c.Deref(p).a - c.Deref(p).a = c.Deref(p).b - c.Deref(p).b = tmp - -def test_usage(): - r: t.CInt = double_export(21) - g: t.CInt = Color.GREEN - u: t.CFloat = Value.f -`; - const testDir = path.join(__dirname, '__viper_keywords_test__'); - const fs = require('fs'); - fs.mkdirSync(testDir, { recursive: true }); - const testFile = path.join(testDir, 'keywords.vp'); - fs.writeFileSync(testFile, code, 'utf8'); - - try { - const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); - const program = new Program(importResolver, configOptions, serviceProvider); - const uri = UriEx.file(testFile); - program.setTrackedFiles([uri]); - while (program.analyze()) { - // 循环 - } - - const diagnostics = getDiagnostics(program, configOptions, uri); - console.log('=== keywords.vp diagnostics ==='); - diagnostics.forEach((d) => console.log(` ${d}`)); - - // 期望没有任何错误级诊断(语法错误/类型错误都不允许)。 - const errors = diagnostics.filter((d) => d.startsWith('[category=0]')); - assert.strictEqual( - errors.length, - 0, - `unexpected error diagnostics: ${JSON.stringify(errors)}` - ); - - program.dispose(); - } finally { - fs.rmSync(testDir, { recursive: true, force: true }); - } - serviceProvider.dispose(); -}); - -test('Viper 修饰关键字 inline/export/extern 单独使用报错', () => { - const tempFile = new RealTempFile(); - const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); - const projectRoot = UriEx.file(TRANSPYC_INCLUDES); - 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 code = `import t - -inline -export = 5 -extern + 1 - -inline def ok(x: t.CInt) -> t.CInt: - return x -`; - const testDir = path.join(__dirname, '__viper_modifier_test__'); - const fs = require('fs'); - fs.mkdirSync(testDir, { recursive: true }); - const testFile = path.join(testDir, 'modifier.vp'); - fs.writeFileSync(testFile, code, 'utf8'); - - try { - const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); - const program = new Program(importResolver, configOptions, serviceProvider); - const uri = UriEx.file(testFile); - program.setTrackedFiles([uri]); - while (program.analyze()) { - // 循环 - } - - const diagnostics = getDiagnostics(program, configOptions, uri); - console.log('=== modifier.vp diagnostics ==='); - diagnostics.forEach((d) => console.log(` ${d}`)); - - // 期望:3 个"必须与 def 搭配"错误(inline / export / extern 单独使用), - // 而 `inline def ok(...)` 不报错。 - const modifierErrors = diagnostics.filter( - (d) => d.includes('必须与 def 搭配') || d.includes('must be used with') - ); - assert.strictEqual( - modifierErrors.length, - 3, - `expected 3 modifier errors, got: ${JSON.stringify(diagnostics)}` - ); - - program.dispose(); - } finally { - fs.rmSync(testDir, { recursive: true, force: true }); - } - serviceProvider.dispose(); -}); - -test('Viper def 别名 fn/fun/func/function 与 extern 函数体校验', () => { - const tempFile = new RealTempFile(); - const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); - const projectRoot = UriEx.file(TRANSPYC_INCLUDES); - 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 code = `import t - -# def 别名 -fn f1(x: t.CInt) -> t.CInt: - return x - -fun f2(x: t.CInt) -> t.CInt: - return x - -func f3(x: t.CInt) -> t.CInt: - return x - -function f4(x: t.CInt) -> t.CInt: - return x - -# extern:函数体必须为 pass 或 None -extern def e_ok1(x: t.CInt) -> t.CInt: - pass - -extern def e_ok2(x: t.CInt) -> t.CInt: - None - -extern def e_bad(x: t.CInt) -> t.CInt: - return x - -# extern 声明可以省略函数体(无冒号、无 pass),": pass" 方式保留 -extern def e_no_body(x: t.CInt) -> t.CInt - -# extern + fn 别名,同样可以省略函数体 -extern fn e_fn_no_body(x: t.CInt) -> t.CInt - -def use(): - a: t.CInt = f1(1) + f2(2) + f3(3) + f4(4) -`; - const testDir = path.join(__dirname, '__viper_alias_extern_test__'); - const fs = require('fs'); - fs.mkdirSync(testDir, { recursive: true }); - const testFile = path.join(testDir, 'alias_extern.vp'); - fs.writeFileSync(testFile, code, 'utf8'); - - try { - const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); - const program = new Program(importResolver, configOptions, serviceProvider); - const uri = UriEx.file(testFile); - program.setTrackedFiles([uri]); - while (program.analyze()) { - // 循环 - } - - const diagnostics = getDiagnostics(program, configOptions, uri); - console.log('=== alias_extern.vp diagnostics ==='); - diagnostics.forEach((d) => console.log(` ${d}`)); - - // 期望恰好 1 个 extern 函数体错误(e_bad),fn/fun/func/function 别名与 - // extern pass/None 函数体都不应报错。 - const externErrors = diagnostics.filter( - (d) => d.includes('必须为 pass 或 None') || d.includes("body of only 'pass' or 'None'") - ); - assert.strictEqual( - externErrors.length, - 1, - `expected 1 extern body error, got: ${JSON.stringify(diagnostics)}` - ); - - program.dispose(); - } finally { - fs.rmSync(testDir, { recursive: true, force: true }); - } - serviceProvider.dispose(); -}); - -test('Viper fn/fun/func/function 单独使用报错,后跟 :/=/ 保持标识符', () => { - const tempFile = new RealTempFile(); - const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); - const projectRoot = UriEx.file(TRANSPYC_INCLUDES); - 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 code = `import t - -# fn 行尾单独使用 → 视为 def,报缺函数名 -fn - -# fun 后跟 '=' → 保持标识符(赋值,不是关键字) -fun = 5 - -# func 后跟 '(' → 保持标识符(调用,不是关键字) -func(1) - -# function 后跟 ':' → 保持标识符(注解声明,不是关键字) -function: t.CInt = 0 - -# 正常用法不受影响 -fn ok(x: t.CInt) -> t.CInt: - return x -`; - const testDir = path.join(__dirname, '__viper_fn_keyword_test__'); - const fs = require('fs'); - fs.mkdirSync(testDir, { recursive: true }); - const testFile = path.join(testDir, 'fn_keyword.vp'); - fs.writeFileSync(testFile, code, 'utf8'); - - try { - const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); - const program = new Program(importResolver, configOptions, serviceProvider); - const uri = UriEx.file(testFile); - program.setTrackedFiles([uri]); - while (program.analyze()) { - // 循环 - } - - const diagnostics = getDiagnostics(program, configOptions, uri); - console.log('=== fn_keyword.vp diagnostics ==='); - diagnostics.forEach((d) => console.log(` ${d}`)); - - // 期望恰好 1 个错误级诊断:fn 行尾单独使用报"缺函数名"; - // fun=/func(/function: 保持标识符不应产生语法/类型错误。 - const errors = diagnostics.filter((d) => d.startsWith('[category=0]')); - assert.strictEqual( - errors.length, - 1, - `expected exactly 1 error (fn alone), got: ${JSON.stringify(diagnostics)}` - ); - assert.ok( - errors[0].includes('函数名') || errors[0].includes('function name'), - `expected missing-function-name error: ${JSON.stringify(errors)}` - ); - - program.dispose(); - } finally { - fs.rmSync(testDir, { recursive: true, force: true }); - } - serviceProvider.dispose(); -}); - -test('builtins.vp 中 Asm/LLVMIR/AsmInp/AsmOut/LInp/LOut 签名可用且不报未定义', () => { - const tempFile = new RealTempFile(); - const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); - const projectRoot = UriEx.file(TRANSPYC_INCLUDES); - 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 code = `import t - -def test_builtins_asm_llvmir(): - r: t.CInt = 0 - # Asm:基础 + 输出/输入操作数 + 破坏列表 - Asm("nop") - Asm(f"mov {AsmOut(r, t.ASM_DESCR.OUTPUT_REG)}, rdi", op=[t.ASM_DESCR.CLOBBER_MEMORY]) - Asm(f"mov eax, {AsmInp(r, t.ASM_DESCR.REG_ANY)}") - # LLVMIR:LInp/LOut 标记操作数 - LLVMIR(f"{LOut(r)} = add i32 {LInp(r)}, {LInp(r)}") -`; - const testDir = path.join(__dirname, '__viper_builtins_asm_test__'); - const fs = require('fs'); - fs.mkdirSync(testDir, { recursive: true }); - const testFile = path.join(testDir, 'builtins_asm.vp'); - fs.writeFileSync(testFile, code, 'utf8'); - - try { - const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); - const program = new Program(importResolver, configOptions, serviceProvider); - const uri = UriEx.file(testFile); - program.setTrackedFiles([uri]); - while (program.analyze()) { - // 循环 - } - - const diagnostics = getDiagnostics(program, configOptions, uri); - console.log('=== builtins_asm.vp diagnostics ==='); - diagnostics.forEach((d) => console.log(` ${d}`)); - - // 期望没有任何错误级诊断:Asm/LLVMIR/AsmInp/AsmOut/LInp/LOut 由隐式 - // builtins(builtins.vpi)提供,无需 import 即可直接使用。 - const errors = diagnostics.filter((d) => d.startsWith('[category=0]')); - assert.strictEqual( - errors.length, - 0, - `unexpected error diagnostics: ${JSON.stringify(diagnostics)}` - ); - - program.dispose(); - } finally { - fs.rmSync(testDir, { recursive: true, force: true }); - } - serviceProvider.dispose(); -}); - -test('Viper len 接受任意值并返回 t.SizeT', () => { - const tempFile = new RealTempFile(); - const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); - const projectRoot = UriEx.file(TRANSPYC_INCLUDES); - 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 code = `import t - -class Vec3: - def __init__(self, x: t.CInt, y: t.CInt, z: t.CInt): - self.x = x - self.y = y - self.z = z - - def __len__(self) -> t.CInt: - return 3 - -class NoLen: - a: t.CInt = 0 - -def test_len_any(): - n: t.CInt = 5 - f: t.CFloat = 3.5 - p: t.CPtr = None - s: str = "hello" - v = Vec3(1, 2, 3) - nl = NoLen() - # len 对 t.CType 原生类型(int/float/指针/str):返回 t.SizeT,不报错 - a: t.CSizeT = len(n) - b: t.CSizeT = len(f) - c: t.CSizeT = len(p) - d: t.CSizeT = len(s) - # len 优先调用 __len__:有 __len__ 的结构体返回其返回类型 - e: t.CSizeT = len(v) - # 无 __len__ 的结构体:报错 - bad = len(nl) -`; - const testDir = path.join(__dirname, '__viper_len_test__'); - const fs = require('fs'); - fs.mkdirSync(testDir, { recursive: true }); - const testFile = path.join(testDir, 'len_any.vp'); - fs.writeFileSync(testFile, code, 'utf8'); - - try { - const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); - const program = new Program(importResolver, configOptions, serviceProvider); - const uri = UriEx.file(testFile); - program.setTrackedFiles([uri]); - while (program.analyze()) { - // 循环 - } - - const diagnostics = getDiagnostics(program, configOptions, uri); - console.log('=== len_any.vp diagnostics ==='); - diagnostics.forEach((d) => console.log(` ${d}`)); - - // 期望恰好 1 个错误级诊断:无 __len__ 的结构体 NoLen 使用 len() 报错; - // t.CType 原生类型(int/float/指针/str)豁免;有 __len__ 的 Vec3 返回其返回类型。 - const errors = diagnostics.filter((d) => d.startsWith('[category=0]')); - assert.strictEqual( - errors.length, - 1, - `expected exactly 1 error (NoLen without __len__), got: ${JSON.stringify(diagnostics)}` - ); - assert.ok( - errors[0].includes('__len__'), - `expected __len__ error message: ${JSON.stringify(errors)}` - ); - - program.dispose(); - } finally { - fs.rmSync(testDir, { recursive: true, force: true }); - } - serviceProvider.dispose(); -}); - -test('extern/export 支持直接声明形式 extern A(...)(无 def)', () => { - const tempFile = new RealTempFile(); - const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); - const projectRoot = UriEx.file(TRANSPYC_INCLUDES); - 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 code = `import t - -# extern/export 直接声明形式(无 def) -extern E1(x: t.CInt) -> t.CInt -extern E2(x: t.CInt) -> t.CInt: pass -export X1(x: t.CInt) -> t.CInt -export X2(x: t.CInt) -> t.CInt: pass -# def 形式保留 -extern def E3(x: t.CInt) -> t.CInt -export def X3(x: t.CInt) -> t.CInt -# extern + fn 别名(无函数体)仍可用 -extern fn E4(x: t.CInt) -> t.CInt - -def use(): - a: t.CInt = E1(1) + X1(2) + E3(3) + X3(4) -`; - const testDir = path.join(__dirname, '__viper_extern_direct_test__'); - const fs = require('fs'); - fs.mkdirSync(testDir, { recursive: true }); - const testFile = path.join(testDir, 'extern_direct.vp'); - fs.writeFileSync(testFile, code, 'utf8'); - - try { - const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); - const program = new Program(importResolver, configOptions, serviceProvider); - const uri = UriEx.file(testFile); - program.setTrackedFiles([uri]); - while (program.analyze()) { - // 循环 - } - - const diagnostics = getDiagnostics(program, configOptions, uri); - console.log('=== extern_direct.vp diagnostics ==='); - diagnostics.forEach((d) => console.log(` ${d}`)); - - // 期望没有任何错误级诊断:extern/export 直接声明形式与 def 形式均可。 - const errors = diagnostics.filter((d) => d.startsWith('[category=0]')); - assert.strictEqual( - errors.length, - 0, - `unexpected error diagnostics: ${JSON.stringify(diagnostics)}` - ); - - program.dispose(); - } finally { - fs.rmSync(testDir, { recursive: true, force: true }); - } - serviceProvider.dispose(); -}); - -test('Viper 内置 double 类型(复刻 float,64 位)可用', () => { - const tempFile = new RealTempFile(); - const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); - const projectRoot = UriEx.file(TRANSPYC_INCLUDES); - 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 code = `import t - -def test_double(): - # float 字面量 → double(32 位 → 64 位,无损) - d: double = 1.5 - # double 运算返回 double - e: double = d * 2.0 - # double 构造 - f: double = double(3) - # double + int → double(整数位宽 <= 64 位浮点,无损) - g: double = d + 1 - # 内置 double → t.CDouble - h: t.CDouble = d -`; - const testDir = path.join(__dirname, '__viper_double_test__'); - const fs = require('fs'); - fs.mkdirSync(testDir, { recursive: true }); - const testFile = path.join(testDir, 'double_type.vp'); - fs.writeFileSync(testFile, code, 'utf8'); - - try { - const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); - const program = new Program(importResolver, configOptions, serviceProvider); - const uri = UriEx.file(testFile); - program.setTrackedFiles([uri]); - while (program.analyze()) { - // 循环 - } - - const diagnostics = getDiagnostics(program, configOptions, uri); - console.log('=== double_type.vp diagnostics ==='); - diagnostics.forEach((d) => console.log(` ${d}`)); - - // 期望没有任何错误级诊断:double 类型、运算、构造及 t.CDouble 转换均可用。 - const errors = diagnostics.filter((d) => d.startsWith('[category=0]')); - assert.strictEqual( - errors.length, - 0, - `unexpected error diagnostics: ${JSON.stringify(diagnostics)}` - ); - - program.dispose(); - } finally { - fs.rmSync(testDir, { recursive: true, force: true }); - } - serviceProvider.dispose(); -}); +/* + * viperGenericTypeVarCall.test.ts + * + * 验证 Viper 泛型类方法中 `T(v)`(用类型变量做 C 风格强转)不应报 + * "Object of type TypeVar is not callable" / "Attribute __call__ is unknown"。 + * + * 场景:TransPyC/Test/GenericTest/App/main.vp + * class Box[T](): + * value: T + * def __init__(self, v: T): + * self.value = T(v) # ← T(v) 类型变量调用 + * + * 运行:npx jest src/tests/viperGenericTypeVarCall.test.ts --runInBand + */ + +import assert from 'assert'; +import * as path from 'path'; + +import { ImportResolver } from '../analyzer/importResolver'; +import { Program } from '../analyzer/program'; +import { ParseTreeWalker } from '../analyzer/parseTreeWalker'; +import { TypeEvaluator } from '../analyzer/typeEvaluatorTypes'; +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 { convertOffsetToPosition } from '../common/positionUtils'; +import { MemberAccessNode } from '../parser/parseNodes'; + +// 允许 vipershed-fallback(t.vpi/c.vpi 存根)被定位。 +(global as any).__rootDirectory = path.resolve(); + +const TRANSPYC_INCLUDES = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes'; +const GENERIC_TEST = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\Test\\GenericTest\\App\\main.vp'; + +function buildProgram(targetPath: string) { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const projectRoot = UriEx.file(path.dirname(targetPath)); + 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(targetPath); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环直到分析完成 + } + + return { program, serviceProvider, execEnv, uri, configOptions }; +} + +function getDiagnostics(program: Program, configOptions: ConfigOptions, uri: any): string[] { + const sourceFile = program.getSourceFile(uri); + assert(sourceFile, 'source file not found'); + return (sourceFile.getDiagnostics(configOptions) || []).map((diag) => { + const line = diag.range ? diag.range.start.line + 1 : -1; + return `[category=${diag.category}][line=${line}] ${diag.message}`; + }); +} + +test('GenericTest 泛型类 Box 中 T(v) 类型变量调用不报 not callable', () => { + const { program, serviceProvider, uri, configOptions } = buildProgram(GENERIC_TEST); + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== GenericTest not-callable diagnostics ==='); + const notCallable = diagnostics.filter( + (d) => d.includes('not callable') || d.includes('not callable') || d.includes('不可调用') + ); + notCallable.forEach((d) => console.log(` ${d}`)); + + assert.strictEqual( + notCallable.length, + 0, + `unexpected not-callable errors: ${JSON.stringify(notCallable)}` + ); + + program.dispose(); + serviceProvider.dispose(); +}); + +test('最小复现:泛型类方法中 T(v) 类型变量调用不报 not callable', () => { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const projectRoot = UriEx.file(TRANSPYC_INCLUDES); + 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 code = `import t + +class Box[T](): + value: T + + def __init__(self, v: T): + self.value = T(v) + + def get(self) -> T: + return self.value + +def make_box(): + b = Box(42) + return b.get() +`; + const testDir = path.join(__dirname, '__typevar_call_test__'); + const fs = require('fs'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'typevar.vp'); + fs.writeFileSync(testFile, code, 'utf8'); + + try { + const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); + const program = new Program(importResolver, configOptions, serviceProvider); + const uri = UriEx.file(testFile); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== typevar.vp diagnostics ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + const notCallable = diagnostics.filter( + (d) => d.includes('not callable') || d.includes('不可调用') || d.includes('__call__') + ); + assert.strictEqual( + notCallable.length, + 0, + `unexpected not-callable errors: ${JSON.stringify(diagnostics)}` + ); + + } finally { + fs.rmSync(testDir, { recursive: true, force: true }); + } + serviceProvider.dispose(); +}); + + + + +test('PEP 695 显式类型实参 double[t.CInt](21) 不报不可下标', () => { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const projectRoot = UriEx.file(TRANSPYC_INCLUDES); + 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 code = `import t + +def double[T](x: int) -> int: + return x * 2 + +def test_explicit_type_arg(): + r: t.CInt = double[t.CInt](21) +`; + const testDir = path.join(__dirname, '__typevar_call_test__'); + const fs = require('fs'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'explicit_type_arg.vp'); + fs.writeFileSync(testFile, code, 'utf8'); + + try { + const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); + const program = new Program(importResolver, configOptions, serviceProvider); + const uri = UriEx.file(testFile); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== explicit_type_arg.vp diagnostics ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + const notSubscriptable = diagnostics.filter( + (d) => d.includes('not subscriptable') || d.includes('不可下标') + ); + assert.strictEqual( + notSubscriptable.length, + 0, + `unexpected subscriptable errors: ${JSON.stringify(diagnostics)}` + ); + + } finally { + fs.rmSync(testDir, { recursive: true, force: true }); + } + serviceProvider.dispose(); +}); + + + +test('c.Deref 对 IntPair | t.CPtr 联合返回结构体,成员访问解析为 CInt', () => { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const projectRoot = UriEx.file(TRANSPYC_INCLUDES); + 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 code = `import t +import c + +class IntPair: + a: t.CInt + b: t.CInt + +def generic_swap[T](p: IntPair | t.CPtr): + tmp: t.CInt = c.Deref(p).a + c.Deref(p).a = c.Deref(p).b + c.Deref(p).b = tmp + bad: t.CInt = c.Deref(p).zzz +`; + const testDir = path.join(__dirname, '__deref_test__'); + const fs = require('fs'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'deref.vp'); + fs.writeFileSync(testFile, code, 'utf8'); + + try { + const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); + const program = new Program(importResolver, configOptions, serviceProvider); + const uri = UriEx.file(testFile); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== deref.vp diagnostics ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + // 期望恰好 1 条"没有属性":c.Deref(p).zzz 应报错(证明 Deref 结果是 + // 具体结构体 IntPair 而非 Unknown——Unknown 上访问任何成员都静默通过), + // 而 c.Deref(p).a / .b 都不应报。 + const noAttr = diagnostics.filter( + (d) => + d.includes('无法访问') || + d.includes('没有属性') || + d.includes('has no attribute') || + d.includes('未知的属性') + ); + assert.strictEqual( + noAttr.length, + 1, + `expected exactly 1 no-attribute error, got: ${JSON.stringify(diagnostics)}` + ); + assert.ok(noAttr[0].includes('zzz'), `expected zzz error: ${JSON.stringify(diagnostics)}`); + + } finally { + fs.rmSync(testDir, { recursive: true, force: true }); + } + serviceProvider.dispose(); +}); + + + +test('Viper 关键字 inline/export/extern/state/enum/rnum/struct 解析与语义', () => { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const projectRoot = UriEx.file(TRANSPYC_INCLUDES); + 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 code = `import t +import c + +# 单独的 struct 关键字(等价 class) +struct IntPair: + a: t.CInt + b: t.CInt + +# 修饰关键字 + def(等价 t.CInline / t.CExport / t.CExtern) +inline def double_inline(x: t.CInt) -> t.CInt: + return x * 2 + +export def double_export(x: t.CInt) -> t.CInt: + return x * 2 + +extern def double_extern(x: t.CInt) -> t.CInt: + pass + +# state 不是关键字,仍可作字段名/成员名 +state: t.CInt = 0 + +# enum / rnum + class(等价 t.CEnum / t.REnum) +enum class Color: + RED: t.CInt = 0 + GREEN: t.CInt = 1 + +rnum class LLVMType: + class Int: + bits: t.CInt + class Ptr: + pointee: t.CPtr + +# enum / rnum 单独使用(等价 class) +enum A: + k: t.State + +rnum B: + class Inner: + x: t.CInt + +# union + class(等价 t.CUnion 风格) +union class Value: + i: t.CInt + f: t.CFloat + +# union 单独使用(等价 class) +union D: + x: t.CInt + +def swap(p: IntPair | t.CPtr): + tmp: t.CInt = c.Deref(p).a + c.Deref(p).a = c.Deref(p).b + c.Deref(p).b = tmp + +def test_usage(): + r: t.CInt = double_export(21) + g: t.CInt = Color.GREEN + u: t.CFloat = Value.f +`; + const testDir = path.join(__dirname, '__viper_keywords_test__'); + const fs = require('fs'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'keywords.vp'); + fs.writeFileSync(testFile, code, 'utf8'); + + try { + const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); + const program = new Program(importResolver, configOptions, serviceProvider); + const uri = UriEx.file(testFile); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== keywords.vp diagnostics ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + // 期望没有任何错误级诊断(语法错误/类型错误都不允许)。 + const errors = diagnostics.filter((d) => d.startsWith('[category=0]')); + assert.strictEqual( + errors.length, + 0, + `unexpected error diagnostics: ${JSON.stringify(errors)}` + ); + + } finally { + fs.rmSync(testDir, { recursive: true, force: true }); + } + serviceProvider.dispose(); +}); + + +test('Viper 修饰关键字 inline/export/extern 单独使用报错', () => { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const projectRoot = UriEx.file(TRANSPYC_INCLUDES); + 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 code = `import t + +inline +export = 5 +extern + 1 + +inline def ok(x: t.CInt) -> t.CInt: + return x +`; + const testDir = path.join(__dirname, '__viper_modifier_test__'); + const fs = require('fs'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'modifier.vp'); + fs.writeFileSync(testFile, code, 'utf8'); + + try { + const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); + const program = new Program(importResolver, configOptions, serviceProvider); + const uri = UriEx.file(testFile); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== modifier.vp diagnostics ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + // 期望:3 个"必须与 def 搭配"错误(inline / export / extern 单独使用), + // 而 `inline def ok(...)` 不报错。 + const modifierErrors = diagnostics.filter( + (d) => d.includes('必须与 def 搭配') || d.includes('must be used with') + ); + assert.strictEqual( + modifierErrors.length, + 3, + `expected 3 modifier errors, got: ${JSON.stringify(diagnostics)}` + ); + + } finally { + fs.rmSync(testDir, { recursive: true, force: true }); + } + serviceProvider.dispose(); +}); + + +test('Viper def 别名 fn/fun/func/function 与 extern 函数体校验', () => { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const projectRoot = UriEx.file(TRANSPYC_INCLUDES); + 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 code = `import t + +# def 别名 +fn f1(x: t.CInt) -> t.CInt: + return x + +fun f2(x: t.CInt) -> t.CInt: + return x + +func f3(x: t.CInt) -> t.CInt: + return x + +function f4(x: t.CInt) -> t.CInt: + return x + +# extern:函数体必须为 pass 或 None +extern def e_ok1(x: t.CInt) -> t.CInt: + pass + +extern def e_ok2(x: t.CInt) -> t.CInt: + None + +extern def e_bad(x: t.CInt) -> t.CInt: + return x + +# extern 声明可以省略函数体(无冒号、无 pass),": pass" 方式保留 +extern def e_no_body(x: t.CInt) -> t.CInt + +# extern + fn 别名,同样可以省略函数体 +extern fn e_fn_no_body(x: t.CInt) -> t.CInt + +def use(): + a: t.CInt = f1(1) + f2(2) + f3(3) + f4(4) +`; + const testDir = path.join(__dirname, '__viper_alias_extern_test__'); + const fs = require('fs'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'alias_extern.vp'); + fs.writeFileSync(testFile, code, 'utf8'); + + try { + const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); + const program = new Program(importResolver, configOptions, serviceProvider); + const uri = UriEx.file(testFile); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== alias_extern.vp diagnostics ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + // 期望恰好 1 个 extern 函数体错误(e_bad),fn/fun/func/function 别名与 + // extern pass/None 函数体都不应报错。 + const externErrors = diagnostics.filter( + (d) => d.includes('必须为 pass 或 None') || d.includes("body of only 'pass' or 'None'") + ); + assert.strictEqual( + externErrors.length, + 1, + `expected 1 extern body error, got: ${JSON.stringify(diagnostics)}` + ); + + } finally { + fs.rmSync(testDir, { recursive: true, force: true }); + } + serviceProvider.dispose(); +}); + + +test('Viper fn/fun/func/function 单独使用报错,后跟 :/=/ 保持标识符', () => { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const projectRoot = UriEx.file(TRANSPYC_INCLUDES); + 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 code = `import t + +# fn 行尾单独使用 → 视为 def,报缺函数名 +fn + +# fun 后跟 '=' → 保持标识符(赋值,不是关键字) +fun = 5 + +# func 后跟 '(' → 保持标识符(调用,不是关键字) +func(1) + +# function 后跟 ':' → 保持标识符(注解声明,不是关键字) +function: t.CInt = 0 + +# 正常用法不受影响 +fn ok(x: t.CInt) -> t.CInt: + return x +`; + const testDir = path.join(__dirname, '__viper_fn_keyword_test__'); + const fs = require('fs'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'fn_keyword.vp'); + fs.writeFileSync(testFile, code, 'utf8'); + + try { + const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); + const program = new Program(importResolver, configOptions, serviceProvider); + const uri = UriEx.file(testFile); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== fn_keyword.vp diagnostics ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + // 期望恰好 1 个错误级诊断:fn 行尾单独使用报"缺函数名"; + // fun=/func(/function: 保持标识符不应产生语法/类型错误。 + const errors = diagnostics.filter((d) => d.startsWith('[category=0]')); + assert.strictEqual( + errors.length, + 1, + `expected exactly 1 error (fn alone), got: ${JSON.stringify(diagnostics)}` + ); + assert.ok( + errors[0].includes('函数名') || errors[0].includes('function name'), + `expected missing-function-name error: ${JSON.stringify(errors)}` + ); + + } finally { + fs.rmSync(testDir, { recursive: true, force: true }); + } + serviceProvider.dispose(); +}); + + +test('builtins.vp 中 Asm/LLVMIR/AsmInp/AsmOut/LInp/LOut 签名可用且不报未定义', () => { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const projectRoot = UriEx.file(TRANSPYC_INCLUDES); + 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 code = `import t + +def test_builtins_asm_llvmir(): + r: t.CInt = 0 + # Asm:基础 + 输出/输入操作数 + 破坏列表 + Asm("nop") + Asm(f"mov {AsmOut(r, t.ASM_DESCR.OUTPUT_REG)}, rdi", op=[t.ASM_DESCR.CLOBBER_MEMORY]) + Asm(f"mov eax, {AsmInp(r, t.ASM_DESCR.REG_ANY)}") + # LLVMIR:LInp/LOut 标记操作数 + LLVMIR(f"{LOut(r)} = add i32 {LInp(r)}, {LInp(r)}") +`; + const testDir = path.join(__dirname, '__viper_builtins_asm_test__'); + const fs = require('fs'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'builtins_asm.vp'); + fs.writeFileSync(testFile, code, 'utf8'); + + try { + const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); + const program = new Program(importResolver, configOptions, serviceProvider); + const uri = UriEx.file(testFile); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== builtins_asm.vp diagnostics ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + // 期望没有任何错误级诊断:Asm/LLVMIR/AsmInp/AsmOut/LInp/LOut 由隐式 + // builtins(builtins.vpi)提供,无需 import 即可直接使用。 + const errors = diagnostics.filter((d) => d.startsWith('[category=0]')); + assert.strictEqual( + errors.length, + 0, + `unexpected error diagnostics: ${JSON.stringify(diagnostics)}` + ); + + } finally { + fs.rmSync(testDir, { recursive: true, force: true }); + } + serviceProvider.dispose(); +}); + + +test('Viper len 接受任意值并返回 t.SizeT', () => { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const projectRoot = UriEx.file(TRANSPYC_INCLUDES); + 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 code = `import t + +class Vec3: + def __init__(self, x: t.CInt, y: t.CInt, z: t.CInt): + self.x = x + self.y = y + self.z = z + + def __len__(self) -> t.CInt: + return 3 + +class NoLen: + a: t.CInt = 0 + +def test_len_any(): + n: t.CInt = 5 + f: t.CFloat = 3.5 + p: t.CPtr = None + s: str = "hello" + v = Vec3(1, 2, 3) + nl = NoLen() + # len 对 t.CType 原生类型(int/float/指针/str):返回 t.SizeT,不报错 + a: t.CSizeT = len(n) + b: t.CSizeT = len(f) + c: t.CSizeT = len(p) + d: t.CSizeT = len(s) + # len 优先调用 __len__:有 __len__ 的结构体返回其返回类型 + e: t.CSizeT = len(v) + # 无 __len__ 的结构体:报错 + bad = len(nl) +`; + const testDir = path.join(__dirname, '__viper_len_test__'); + const fs = require('fs'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'len_any.vp'); + fs.writeFileSync(testFile, code, 'utf8'); + + try { + const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); + const program = new Program(importResolver, configOptions, serviceProvider); + const uri = UriEx.file(testFile); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== len_any.vp diagnostics ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + // 期望恰好 1 个错误级诊断:无 __len__ 的结构体 NoLen 使用 len() 报错; + // t.CType 原生类型(int/float/指针/str)豁免;有 __len__ 的 Vec3 返回其返回类型。 + const errors = diagnostics.filter((d) => d.startsWith('[category=0]')); + assert.strictEqual( + errors.length, + 1, + `expected exactly 1 error (NoLen without __len__), got: ${JSON.stringify(diagnostics)}` + ); + assert.ok( + errors[0].includes('__len__'), + `expected __len__ error message: ${JSON.stringify(errors)}` + ); + + } finally { + fs.rmSync(testDir, { recursive: true, force: true }); + } + serviceProvider.dispose(); +}); + + +test('extern/export 支持直接声明形式 extern A(...)(无 def)', () => { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const projectRoot = UriEx.file(TRANSPYC_INCLUDES); + 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 code = `import t + +# extern/export 直接声明形式(无 def) +extern E1(x: t.CInt) -> t.CInt +extern E2(x: t.CInt) -> t.CInt: pass +export X1(x: t.CInt) -> t.CInt +export X2(x: t.CInt) -> t.CInt: pass +# def 形式保留 +extern def E3(x: t.CInt) -> t.CInt +export def X3(x: t.CInt) -> t.CInt +# extern + fn 别名(无函数体)仍可用 +extern fn E4(x: t.CInt) -> t.CInt + +def use(): + a: t.CInt = E1(1) + X1(2) + E3(3) + X3(4) +`; + const testDir = path.join(__dirname, '__viper_extern_direct_test__'); + const fs = require('fs'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'extern_direct.vp'); + fs.writeFileSync(testFile, code, 'utf8'); + + try { + const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); + const program = new Program(importResolver, configOptions, serviceProvider); + const uri = UriEx.file(testFile); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== extern_direct.vp diagnostics ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + // 期望没有任何错误级诊断:extern/export 直接声明形式与 def 形式均可。 + const errors = diagnostics.filter((d) => d.startsWith('[category=0]')); + assert.strictEqual( + errors.length, + 0, + `unexpected error diagnostics: ${JSON.stringify(diagnostics)}` + ); + + } finally { + fs.rmSync(testDir, { recursive: true, force: true }); + } + serviceProvider.dispose(); +}); + + +test('Viper 内置 double 类型(复刻 float,64 位)可用', () => { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const projectRoot = UriEx.file(TRANSPYC_INCLUDES); + 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 code = `import t + +def test_double(): + # float 字面量 → double(32 位 → 64 位,无损) + d: double = 1.5 + # double 运算返回 double + e: double = d * 2.0 + # double 构造 + f: double = double(3) + # double + int → double(整数位宽 <= 64 位浮点,无损) + g: double = d + 1 + # 内置 double → t.CDouble + h: t.CDouble = d +`; + const testDir = path.join(__dirname, '__viper_double_test__'); + const fs = require('fs'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'double_type.vp'); + fs.writeFileSync(testFile, code, 'utf8'); + + try { + const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); + const program = new Program(importResolver, configOptions, serviceProvider); + const uri = UriEx.file(testFile); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== double_type.vp diagnostics ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + // 期望没有任何错误级诊断:double 类型、运算、构造及 t.CDouble 转换均可用。 + const errors = diagnostics.filter((d) => d.startsWith('[category=0]')); + assert.strictEqual( + errors.length, + 0, + `unexpected error diagnostics: ${JSON.stringify(diagnostics)}` + ); + + } finally { + fs.rmSync(testDir, { recursive: true, force: true }); + } + serviceProvider.dispose(); +}); + + +test('Viper 构造:__new__ 返回 实例|CPtr 联合时按 __init__ 校验参数(如 FunctionDef 7 参构造)', () => { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const projectRoot = UriEx.file(TRANSPYC_INCLUDES); + 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 code = `import t + +class Foo: + x: t.CInt + + def __new__(self, pool, a: t.CInt, b: t.CInt): + ptr: Foo | t.CPtr = None + return ptr + + def __init__(self, pool, a: t.CInt, b: t.CInt, c: t.CInt = None): + self.x = a + +def test(): + f: Foo | t.CPtr = Foo(None, 1, 2, 3) +`; + const testDir = path.join(__dirname, '__viper_ctor_repro__'); + const fs = require('fs'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'ctor_repro.vp'); + fs.writeFileSync(testFile, code, 'utf8'); + + try { + const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); + const program = new Program(importResolver, configOptions, serviceProvider); + const uri = UriEx.file(testFile); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== ctor_repro.vp diagnostics ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + const errors = diagnostics.filter((d) => d.startsWith('[category=0]')); + assert.strictEqual( + errors.length, + 0, + `unexpected error diagnostics: ${JSON.stringify(diagnostics)}` + ); + + } finally { + fs.rmSync(testDir, { recursive: true, force: true }); + } + serviceProvider.dispose(); +}); + + +test('llvmlite 链式:Value.Ty.Pointee 嵌套成员访问可解析(AssignGenMixin.vp:874)', () => { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const projectRoot = UriEx.file(TRANSPYC_INCLUDES); + 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 code = `import t, c +import llvmlite + +def test(target_alloca: llvmlite.Value | t.CPtr): + target_ty: llvmlite.LLVMType | t.CPtr = target_alloca.Ty.Pointee + elem_ptr: llvmlite.Value | t.CPtr = None + elem_ty: llvmlite.LLVMType | t.CPtr = elem_ptr.Ty.Pointee + +def test_narrow(target_alloca: llvmlite.Value | t.CPtr = None): + if target_alloca is not None: + t1: llvmlite.LLVMType | t.CPtr = target_alloca.Ty.Pointee + if target_alloca.Ty is not None: + t2: llvmlite.LLVMType | t.CPtr = target_alloca.Ty.Pointee + +# Viper fail-fast 报错函数:-> t.State 表示 C 风格 noreturn(不返回,终止流程)。 +def fatal_error(node: t.CInt, msg: str) -> t.State: + pass + +def test_ty_narrow(target_alloca: llvmlite.Value | t.CPtr): + if target_alloca.Ty is None: + fatal_error(0, "变量类型信息缺失") + t5: llvmlite.LLVMType | t.CPtr = target_alloca.Ty.Pointee +`; + const testDir = path.join(__dirname, '__llvmlite_chain__'); + const fs = require('fs'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'chain_repro.vp'); + fs.writeFileSync(testFile, code, 'utf8'); + + try { + const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); + const program = new Program(importResolver, configOptions, serviceProvider); + const uri = UriEx.file(testFile); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== chain_repro.vp diagnostics ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + const errors = diagnostics.filter( + (d) => d.startsWith('[category=0]') || d.startsWith('[category=1]') + ); + assert.strictEqual( + errors.length, + 0, + `unexpected error diagnostics: ${JSON.stringify(diagnostics)}` + ); + + // 防回归断言:`if target_alloca.Ty is None: fatal_error(...)` 后, + // `target_alloca.Ty` 的 hover 类型必须是 CPtr[LLVMType](fatal_error 的 + // t.State/无注解 sys.exit 结尾语义被识别为 NoReturn,None 不泄漏), + // 而非 CPtr[None]。 + const sourceFile = program.getSourceFile(uri); + assert(sourceFile, 'source file not found'); + const parseResults = sourceFile.getParseResults(); + assert(parseResults, 'parse results not found'); + const evaluator = program.evaluator as TypeEvaluator | undefined; + assert(evaluator, 'evaluator not available'); + const ev = evaluator; + const lines = parseResults.parserOutput.lines; + const tyTypes: string[] = []; + let memberAccessCount = 0; + class TyCollector extends ParseTreeWalker { + override visitMemberAccess(node: MemberAccessNode) { + memberAccessCount++; + const memberName = (node.d.member as any)?.d?.value ?? ''; + const pos = convertOffsetToPosition(node.start, lines); + if (memberName !== 'Ty') { + return true; + } + const exprType = ev.getType(node as any); + tyTypes.push( + `L${pos.line + 1} .Ty: ${exprType ? program.printType(exprType) : '(undefined)'}` + ); + return true; + } + } + new TyCollector().walk(parseResults.parserOutput.parseTree); + console.log(`=== .Ty hover 类型 (memberAccess=${memberAccessCount}) ===`); + tyTypes.forEach((t) => console.log(` ${t}`)); + for (const t of tyTypes) { + // 仅断言 test_ty_narrow(fatal_error NoReturn 场景,L19-22)内的 .Ty 非 None。 + // 其他场景(elem_ptr = None 等)的 None 联合属正常语义,不受此断言约束。 + const lineMatch = /L(\d+)/.exec(t); + const line = lineMatch ? parseInt(lineMatch[1], 10) : 0; + if (line >= 19 && line <= 22) { + assert( + !t.includes('None'), + `Ty should not be None-typed (fatal_error must terminate flow): ${t}` + ); + } + } + + program.dispose(); + } finally { + fs.rmSync(testDir, { recursive: true, force: true }); + } + serviceProvider.dispose(); +}); + + +test('真实文件 AssignGenMixin.vp 分析(target_alloca.Ty.Pointee 链式)', () => { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const targetPath = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\TransPyV\\App\\lib\\core\\Gen\\AssignGenMixin.vp'; + const projectRoot = UriEx.file(path.dirname(targetPath)); + const configOptions = new ConfigOptions(projectRoot); + configOptions.internalTestMode = true; + configOptions.initializeTypeCheckingMode('standard'); + + const execEnv = new ExecutionEnvironment( + 'python', + projectRoot, + configOptions.diagnosticRuleSet, + configOptions.defaultPythonVersion, + configOptions.defaultPythonPlatform, + [projectRoot, UriEx.file(TRANSPYC_INCLUDES)], + /* 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(targetPath); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== AssignGenMixin.vp errors (line 840-930) ==='); + const errors = diagnostics.filter( + (d) => + (d.startsWith('[category=0]') || d.startsWith('[category=1]')) && + d.includes('line=84') || + d.includes('line=85') || + d.includes('line=86') || + d.includes('line=87') || + d.includes('line=88') || + d.includes('line=89') || + d.includes('line=90') || + d.includes('line=91') || + d.includes('line=92') + ); + errors.forEach((d) => console.log(` ${d}`)); + console.log(`errors in range: ${errors.length}`); + + program.dispose(); + serviceProvider.dispose(); +}); diff --git a/packages/vipeye-internal/src/tests/viperNullZeroCast.test.ts b/packages/vipeye-internal/src/tests/viperNullZeroCast.test.ts new file mode 100644 index 00000000..794ef7e1 --- /dev/null +++ b/packages/vipeye-internal/src/tests/viperNullZeroCast.test.ts @@ -0,0 +1,128 @@ +/* + * viperNullZeroCast.test.ts + * + * 验证 Viper C 空指针常量(null pointer constant)规则: + * - 整数字面量 0 可隐式转换为任意指针(NULL),包括赋值与函数传参 + * - 其他整数值(含值为 0 的变量)不允许隐式转换为指针,需显式转换 + * - None 仍允许(空指针) + * - 显式转换 t.CInt(addr, t.CPtr) 仍允许 + * + * 运行:npx jest src/tests/viperNullZeroCast.test.ts --runInBand + */ + +import assert from 'assert'; +import * as path from 'path'; + +import { ImportResolver } from '../analyzer/importResolver'; +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'; + +(global as any).__rootDirectory = path.resolve(); + +const TRANSPYC_INCLUDES = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes'; + +function buildProgramForCode(code: string) { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const projectRoot = UriEx.file(TRANSPYC_INCLUDES); + 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 fs = require('fs'); + const testDir = path.join(__dirname, '__null_zero__'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'null_zero.vp'); + fs.writeFileSync(testFile, code, 'utf8'); + + const uri = UriEx.file(testFile); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + const sourceFile = program.getSourceFile(uri); + assert(sourceFile, 'source file not found'); + return { program, configOptions, uri, serviceProvider }; +} + +function getDiagnostics(program: Program, configOptions: ConfigOptions, uri: any): string[] { + const sourceFile = program.getSourceFile(uri); + assert(sourceFile, 'source file not found'); + return (sourceFile.getDiagnostics(configOptions) || []).map((diag) => { + const line = diag.range ? diag.range.start.line + 1 : -1; + return `[category=${diag.category}][line=${line}] ${diag.message}`; + }); +} + +test('整数字面量 0 → Ptr 隐式转换(NULL),其他整数拒绝', () => { + // 注:带注解的变量声明初始化(p: T = v)遵循 Viper TypeSet 语义, + // RHS 被声明类型覆盖不单独校验;本测试覆盖会真实触发赋值检查的 + // 重新赋值(p = v)与函数传参(foo(v))。 + const code = `import t, c + +def takes_ptr(p: t.CInt | t.CPtr) -> t.CInt | t.CPtr: + return p + +def test_zero_cast(): + p: t.CInt | t.CPtr = None + q: t.CInt | t.CPtr = None + p = 0 + p = 1 + x: t.CInt = 0 + p = x + takes_ptr(0) + takes_ptr(1) + addr: t.CUnsignedLong = 0x1234 + p = addr + p = t.CInt(addr, t.CPtr) + takes_ptr(p) + takes_ptr(q) +`; + const { program, configOptions, uri, serviceProvider } = buildProgramForCode(code); + const diagnostics = getDiagnostics(program, configOptions, uri); + + // 允许行:L7/L8 声明 None、L9 重新赋值 0、L13 函数传参 0、L17 显式转换 + for (const okLine of [7, 8, 9, 13, 17]) { + const errs = diagnostics.filter((d) => d.includes(`[line=${okLine}]`)); + assert.strictEqual( + errs.length, + 0, + `L${okLine} 不应报错,实际: ${JSON.stringify(errs)}` + ); + } + + // 拒绝行:L10 重新赋值 1、L12 变量 x、L14 函数传参 1、L16 地址值变量 + for (const badLine of [10, 12, 14, 16]) { + const errs = diagnostics.filter((d) => d.includes(`[line=${badLine}]`)); + assert(errs.length > 0, `L${badLine} 应报"仅允许整数字面量 0"错误,实际诊断: ${JSON.stringify(diagnostics)}`); + const msg = errs[0]; + assert( + msg.includes('仅允许整数字面量 0'), + `L${badLine} 错误信息应提示仅允许 0,实际: ${msg}` + ); + } + + program.dispose(); + serviceProvider.dispose(); +}); diff --git a/packages/vipeye-internal/src/tests/viperREnumKind.test.ts b/packages/vipeye-internal/src/tests/viperREnumKind.test.ts new file mode 100644 index 00000000..a186386f --- /dev/null +++ b/packages/vipeye-internal/src/tests/viperREnumKind.test.ts @@ -0,0 +1,216 @@ +/* + * viperREnumKind.test.ts + * + * 验证 Viper REnum(t.REnum)自带 kind() 方法: + * - REnum 内存布局为 { i32 __tag, }, + * kind() 读取 __tag 判别值,返回 t.CInt(与 ast.AST.kind() 一致)。 + * - 所有 REnum 类实例(含 `MyEnum | t.CPtr` 折叠的 CPtr[MyEnum])均可 + * 访问 .kind 成员(解析为 `() -> int` 函数)并调用 .kind()(返回 int)。 + * + * 运行:npx jest src/tests/viperREnumKind.test.ts --runInBand + */ + +import assert from 'assert'; +import * as path from 'path'; + +import { ImportResolver } from '../analyzer/importResolver'; +import { Program } from '../analyzer/program'; +import { ParseTreeWalker } from '../analyzer/parseTreeWalker'; +import { TypeEvaluator } from '../analyzer/typeEvaluatorTypes'; +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 { convertOffsetToPosition } from '../common/positionUtils'; +import { CallNode, MemberAccessNode } from '../parser/parseNodes'; + +(global as any).__rootDirectory = path.resolve(); + +const TRANSPYC_INCLUDES = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes'; + +function buildProgramForCode(code: string) { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const projectRoot = UriEx.file(TRANSPYC_INCLUDES); + 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 fs = require('fs'); + const testDir = path.join(__dirname, '__renum_kind__'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'renum_kind.vp'); + fs.writeFileSync(testFile, code, 'utf8'); + + const uri = UriEx.file(testFile); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + const sourceFile = program.getSourceFile(uri); + assert(sourceFile, 'source file not found'); + const parseResults = sourceFile.getParseResults(); + assert(parseResults, 'parse results not found'); + + return { program, configOptions, parseResults, uri, serviceProvider }; +} + +function getDiagnostics(program: Program, configOptions: ConfigOptions, uri: any): string[] { + const sourceFile = program.getSourceFile(uri); + assert(sourceFile, 'source file not found'); + return (sourceFile.getDiagnostics(configOptions) || []).map((diag) => { + const line = diag.range ? diag.range.start.line + 1 : -1; + return `[category=${diag.category}][line=${line}] ${diag.message}`; + }); +} + +/** + * 语义 token 辅助:解码 SemanticTokens 相对编码,返回 line 级 token 映射。 + * 返回 Map> + */ +function getSemanticTokens(program: Program, uri: any): Map { + const { SemanticTokenProvider } = require('../languageService/semanticTokenProvider'); + const { LogTracker } = require('../common/logTracker'); + const provider = new SemanticTokenProvider(new LogTracker()); + const result = provider.provideFull(program, uri); + assert(result, 'no semantic tokens produced'); + + const sourceFile = program.getSourceFile(uri); + const lines = sourceFile!.getParseResults()!.text.split('\n'); + + const map = new Map(); + let cursorLine = 0; + let cursorChar = 0; + for (let i = 0; i < result.data.length; i += 5) { + const deltaLine = result.data[i]; + const deltaStart = result.data[i + 1]; + const length = result.data[i + 2]; + const tokenType = result.data[i + 3]; + const tokenMod = result.data[i + 4]; + cursorLine += deltaLine; + cursorChar = deltaLine === 0 ? cursorChar + deltaStart : deltaStart; + const lineText = lines[cursorLine] || ''; + const text = lineText.substring(cursorChar, cursorChar + length); + const entry = { start: cursorChar, text, tokenType, tokenModifiers: tokenMod }; + if (!map.has(cursorLine)) { + map.set(cursorLine, []); + } + map.get(cursorLine)!.push(entry); + } + return map; +} + +test('REnum 自带 kind() 返回 t.CInt(读取 __tag 判别值)', () => { + const code = `import t, c + +class MyEnum(t.REnum): + class Int: + Bits: t.CInt + + class Ptr: + Pointee: MyEnum | t.CPtr + +def test_enum(e: MyEnum | t.CPtr) -> int: + k: t.CInt = e.kind() + k2: t.CInt = e.kind + return k + k2 + +def test_assign() -> int: + e: MyEnum | t.CPtr = MyEnum.Int(32) + if e.kind() == 1: + return 1 + return 0 +`; + const { program, configOptions, parseResults, uri, serviceProvider } = buildProgramForCode(code); + + const evaluator = program.evaluator as TypeEvaluator | undefined; + assert(evaluator, 'evaluator not available'); + const ev = evaluator; + const lines = parseResults.parserOutput.lines; + + // 收集 kind 成员访问与调用类型 + const memberTypes: { line: number; type: string }[] = []; + const callTypes: { line: number; type: string }[] = []; + class Collector extends ParseTreeWalker { + override visitMemberAccess(node: MemberAccessNode) { + const memberName = (node.d.member as any)?.d?.value ?? '?'; + if (memberName !== 'kind') { + return true; + } + const pos = convertOffsetToPosition(node.start, lines); + const t = ev.getType(node as any); + memberTypes.push({ + line: pos.line + 1, + type: t ? program.printType(t) : '(undefined)', + }); + return true; + } + override visitCall(node: CallNode) { + const pos = convertOffsetToPosition(node.start, lines); + const t = ev.getType(node as any); + callTypes.push({ + line: pos.line + 1, + type: t ? program.printType(t) : '(undefined)', + }); + return true; + } + } + new Collector().walk(parseResults.parserOutput.parseTree); + + // 断言 1:e.kind 成员访问解析为函数 () -> int(L11/L12/L17) + const kindMembers = memberTypes.filter((m) => m.type.includes('->')); + console.log('=== .kind 成员类型 ==='); + memberTypes.forEach((m) => console.log(` L${m.line}: ${m.type}`)); + assert(kindMembers.length >= 3, `kind member should resolve to function type: ${JSON.stringify(memberTypes)}`); + + // 断言 2:e.kind() 调用(L11/L17)返回 int + const kindCalls = callTypes.filter((c) => c.line === 11 || c.line === 17); + console.log('=== kind() 调用类型 ==='); + callTypes.forEach((c) => console.log(` L${c.line}: ${c.type}`)); + assert(kindCalls.length >= 2, `kind() calls not found: ${JSON.stringify(callTypes)}`); + kindCalls.forEach((c) => { + assert(c.type.includes('int'), `kind() should return int: ${JSON.stringify(c)}`); + }); + + // 断言 3:不得有 category=0/1 错误(kind 不报 unknown member / not callable) + const diagnostics = getDiagnostics(program, configOptions, uri); + const errors = diagnostics.filter((d) => d.includes('category=0') || d.includes('category=1')); + console.log('=== 错误诊断 ==='); + errors.forEach((d) => console.log(` ${d}`)); + assert.strictEqual(errors.length, 0, `REnum kind should produce no errors: ${JSON.stringify(diagnostics)}`); + + // 断言 4:kind 语义 token 应为 function(2,黄色),而非 variable(3)/property(13) + const tokensByLine = getSemanticTokens(program, uri); + for (const line of [11, 12, 17]) { + const tokens = tokensByLine.get(line - 1); + assert(tokens, `expected tokens on L${line}: ${JSON.stringify([...tokensByLine.keys()])}`); + const kindToken = tokens.find((t) => t.text === 'kind'); + assert(kindToken, `expected "kind" token on L${line}: ${JSON.stringify(tokens)}`); + assert.strictEqual( + kindToken.tokenType, + 2, + `"kind" on L${line} should be function(2), got ${kindToken.tokenType} (${JSON.stringify(kindToken)})` + ); + } + + program.dispose(); + serviceProvider.dispose(); +}); diff --git a/packages/vipeye-internal/src/tests/viperRelImportNoInit.test.ts b/packages/vipeye-internal/src/tests/viperRelImportNoInit.test.ts new file mode 100644 index 00000000..706f2a68 --- /dev/null +++ b/packages/vipeye-internal/src/tests/viperRelImportNoInit.test.ts @@ -0,0 +1,132 @@ +/* + * viperRelImportNoInit.test.ts + * + * 验证 Viper 相对导入在目录无 __init__.vp 时的行为: + * - `from . import X` 且目录中存在 X.vp → 正常解析,不报"命名空间包不完整"警告 + * - `from . import 不存在` → 仍报"命名空间包不完整"警告 + * - `import 空目录`(无 __init__.vp)→ 仍报"命名空间包不完整"警告 + * + * 运行:npx jest src/tests/viperRelImportNoInit.test.ts --runInBand + */ + +import assert from 'assert'; +import * as path from 'path'; + +import { ImportResolver } from '../analyzer/importResolver'; +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'; + +(global as any).__rootDirectory = path.resolve(); + +function buildProgram(files: Record) { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const fs = require('fs'); + const testRoot = path.join(__dirname, '__rel_import__'); + fs.rmSync(testRoot, { recursive: true, force: true }); + for (const [relPath, content] of Object.entries(files)) { + const absPath = path.join(testRoot, relPath); + fs.mkdirSync(path.dirname(absPath), { recursive: true }); + fs.writeFileSync(absPath, content, '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 uris = Object.keys(files).map((relPath) => UriEx.file(path.join(testRoot, relPath))); + const urisMap: Record = {}; + for (const relPath of Object.keys(files)) { + urisMap[relPath] = UriEx.file(path.join(testRoot, relPath)); + } + program.setTrackedFiles(uris); + while (program.analyze()) { + // 循环 + } + + return { program, configOptions, uris, urisMap, testRoot, serviceProvider }; +} + +function getDiagnostics(program: Program, configOptions: ConfigOptions, uri: any): string[] { + const sourceFile = program.getSourceFile(uri); + assert(sourceFile, 'source file not found'); + return (sourceFile.getDiagnostics(configOptions) || []).map((diag) => { + const line = diag.range ? diag.range.start.line + 1 : -1; + return `[category=${diag.category}][line=${line}] ${diag.message}`; + }); +} + +test('目录无 __init__.vp 时,from . import X(X.vp 存在)不报命名空间包警告', () => { + const { program, configOptions, urisMap, testRoot, serviceProvider } = buildProgram({ + 'Call/CallGenBase.vp': `def base_helper(): + return 0 +`, + 'Call/CallGenCBuiltin.vp': `from . import CallGenBase + +def use_it() -> int: + return CallGenBase.base_helper() +`, + 'Call/other.vp': `from . import Nonexistent + +def f(): + return Nonexistent +`, + 'EmptyDir/.keep': ``, + 'main.vp': `import EmptyDir + +def main(): + return 0 +`, + }); + + // 1. from . import CallGenBase(CallGenBase.vp 存在)→ 不应报"命名空间包不完整" + const callGenDiags = getDiagnostics(program, configOptions, urisMap['Call/CallGenCBuiltin.vp']); + assert( + !callGenDiags.some((d) => d.includes('缺少 __init__.vp')), + `from . import CallGenBase 不应报命名空间包警告,实际: ${JSON.stringify(callGenDiags)}` + ); + + // 2. from . import Nonexistent(文件不存在)→ 解析为"未找到"(非命名空间包), + // 不产生命名空间包警告;Nonexistent 使用处报未定义(证明导入确实失败) + const otherDiags = getDiagnostics(program, configOptions, urisMap['Call/other.vp']); + assert( + !otherDiags.some((d) => d.includes('缺少 __init__.vp')), + `from . import Nonexistent 不应报命名空间包警告,实际: ${JSON.stringify(otherDiags)}` + ); + assert( + otherDiags.length > 0, + `from . import Nonexistent 应产生未定义/类型错误,实际: ${JSON.stringify(otherDiags)}` + ); + + // 3. import EmptyDir(目录无 __init__.vp)→ 仍报"命名空间包不完整" + const mainDiags = getDiagnostics(program, configOptions, urisMap['main.vp']); + assert( + mainDiags.some((d) => d.includes('缺少 __init__.vp')), + `import EmptyDir 应报命名空间包警告,实际: ${JSON.stringify(mainDiags)}` + ); + + program.dispose(); + serviceProvider.dispose(); + require('fs').rmSync(testRoot, { recursive: true, force: true }); +}); diff --git a/packages/vipeye-internal/src/tests/viperTyNarrowDump.test.ts b/packages/vipeye-internal/src/tests/viperTyNarrowDump.test.ts new file mode 100644 index 00000000..24a93038 --- /dev/null +++ b/packages/vipeye-internal/src/tests/viperTyNarrowDump.test.ts @@ -0,0 +1,174 @@ +/* + * viperTyNarrowDump.test.ts — 临时复现 target_alloca.Ty 收窄为 CPtr[None] + */ + +import * as path from 'path'; + +import { ImportResolver } from '../analyzer/importResolver'; +import { Program } from '../analyzer/program'; +import { ParseTreeWalker } from '../analyzer/parseTreeWalker'; +import { TypeEvaluator } from '../analyzer/typeEvaluatorTypes'; +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 { convertOffsetToPosition } from '../common/positionUtils'; +import { MemberAccessNode } from '../parser/parseNodes'; + +(global as any).__rootDirectory = path.resolve(); + +const TRANSPYC_INCLUDES = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes'; +const TPV_APP = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\TransPyV\\App'; +const ASSIGN_GEN = + 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\TransPyV\\App\\lib\\core\\Gen\\AssignGenMixin.vp'; + +function buildProgram(targetPath: string, extraPaths: string[]) { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const projectRoot = UriEx.file(path.dirname(targetPath)); + const configOptions = new ConfigOptions(projectRoot); + configOptions.internalTestMode = true; + configOptions.initializeTypeCheckingMode('standard'); + + const execEnv = new ExecutionEnvironment( + 'python', + projectRoot, + configOptions.diagnosticRuleSet, + configOptions.defaultPythonVersion, + configOptions.defaultPythonPlatform, + [projectRoot, ...extraPaths.map((p) => UriEx.file(p))], + /* 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(targetPath); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + const sourceFile = program.getSourceFile(uri); + const parseResults = sourceFile!.getParseResults(); + return { program, configOptions, parseResults, uri, serviceProvider }; +} + +test('AssignGenMixin.vp 守卫后 .Ty/.Pointee 不误收窄为 CPtr[None]', () => { + const { program, configOptions, parseResults, uri, serviceProvider } = buildProgram(ASSIGN_GEN, [ + TRANSPYC_INCLUDES, + TPV_APP, + ]); + + const evaluator = program.evaluator as TypeEvaluator | undefined; + const out: string[] = []; + if (evaluator && parseResults) { + const ev = evaluator; + const lines = parseResults.parserOutput.lines; + const pointeeTypes: string[] = []; + const tyTypes: string[] = []; + class TyCollector extends ParseTreeWalker { + override visitMemberAccess(node: MemberAccessNode) { + const pos = convertOffsetToPosition(node.start, lines); + if (pos.line < 795 || pos.line > 830) { + return true; + } + const memberName = (node.d.member as any)?.d?.value ?? '?'; + const t = ev.getType(node as any); + const printed = t ? program.printType(t) : '(undefined)'; + if (memberName === 'Pointee') { + pointeeTypes.push(printed); + } else if (memberName === 'Ty') { + tyTypes.push(printed); + } + out.push(` L${pos.line + 1} .${memberName}: ${printed}`); + return true; + } + } + new TyCollector().walk(parseResults.parserOutput.parseTree); + + // 核心断言 1:`if target_alloca.Ty is None:` 守卫之后,.Ty 不再被误窄化为 CPtr[None] + //(None 是空指针取值,is None 正测试不改变 CPtr 指针类型本身) + expect(tyTypes.some((s) => s.includes('[None]'))).toBe(false); + expect(tyTypes.every((s) => s === 'CPtr[LLVMType]')).toBe(true); + + // 核心断言 2:守卫之后 .Pointee 可正常解析(原报"无法访问类'None'的属性'Pointee'") + expect(pointeeTypes.some((s) => s === 'Unknown')).toBe(false); + expect(pointeeTypes.every((s) => s === 'CPtr[LLVMType]')).toBe(true); + + // 核心断言 3:L795-830 范围内不应再出现"无法访问/未定义"诊断 + const sourceFile = program.getSourceFile(uri); + const diags = sourceFile!.getDiagnostics(configOptions) || []; + const rangeDiags = diags.filter((d: any) => { + const line = d.range ? d.range.start.line + 1 : -1; + return line >= 795 && line <= 830; + }); + expect(rangeDiags.length).toBe(0); + rangeDiags.forEach((d: any) => { + out.push(` [category=${d.category}][line=${(d.range?.start.line ?? -1) + 1}] ${d.message}`); + }); + } + + require('fs').writeFileSync(path.join(__dirname, '__ty_narrow_dump.txt'), out.join('\n') + '\n', 'utf8'); + program.dispose(); + serviceProvider.dispose(); +}); + +test('AssignGenMixin.vp match 变体模式内 rhs_val.Ty 保持指针形态 CPtr[Struct]', () => { + const { program, configOptions, parseResults, uri, serviceProvider } = buildProgram(ASSIGN_GEN, [ + TRANSPYC_INCLUDES, + TPV_APP, + ]); + + const evaluator = program.evaluator as TypeEvaluator | undefined; + const out: string[] = []; + if (evaluator && parseResults) { + const ev = evaluator; + const lines = parseResults.parserOutput.lines; + const tyByLine: Array<{ line: number; type: string }> = []; + class TyCollector extends ParseTreeWalker { + override visitMemberAccess(node: MemberAccessNode) { + const pos = convertOffsetToPosition(node.start, lines); + if (pos.line < 1525 || pos.line > 1560) { + return true; + } + const memberName = (node.d.member as any)?.d?.value ?? '?'; + const t = ev.getType(node as any); + if (memberName === 'Ty') { + tyByLine.push({ line: pos.line + 1, type: t ? program.printType(t) : '(undefined)' }); + } + out.push(` L${pos.line + 1} .${memberName}: ${t ? program.printType(t) : '(undefined)'}`); + return true; + } + } + new TyCollector().walk(parseResults.parserOutput.parseTree); + + // 核心断言 1:Struct case 体内(L1537/L1540/L1559)的 .Ty 保持指针形态 CPtr[Struct], + // 而非误收窄为值类型 Struct(原导致 build_alloca 等报"Struct 不可分配给 CPtr[LLVMType]") + const caseBodyTy = tyByLine.filter((e) => e.line >= 1537); + expect(caseBodyTy.length).toBeGreaterThan(0); + expect(caseBodyTy.every((e) => e.type === 'CPtr[Struct]')).toBe(true); + // 全范围不允许出现纯值类型 Struct + expect(tyByLine.some((e) => e.type === 'Struct')).toBe(false); + + // 核心断言 2:L1525-1560 范围内不应再出现"不可分配给"诊断 + const sourceFile = program.getSourceFile(uri); + const diags = sourceFile!.getDiagnostics(configOptions) || []; + const rangeDiags = diags.filter((d: any) => { + const line = d.range ? d.range.start.line + 1 : -1; + return line >= 1525 && line <= 1560; + }); + expect(rangeDiags.length).toBe(0); + rangeDiags.forEach((d: any) => { + out.push(` [category=${d.category}][line=${(d.range?.start.line ?? -1) + 1}] ${d.message}`); + }); + } + + require('fs').writeFileSync(path.join(__dirname, '__ty_narrow_dump.txt'), out.join('\n') + '\n', 'utf8'); + program.dispose(); + serviceProvider.dispose(); +}); diff --git a/packages/vipeye-internal/src/tests/viperUnaryPointer.test.ts b/packages/vipeye-internal/src/tests/viperUnaryPointer.test.ts new file mode 100644 index 00000000..8b818b95 --- /dev/null +++ b/packages/vipeye-internal/src/tests/viperUnaryPointer.test.ts @@ -0,0 +1,381 @@ +/* + * viperUnaryPointer.test.ts + * + * 验证 Viper 一元 '*'(解引用)与 '&'(取地址)运算符,与 C 语义一致: + * - `&x`:取地址,返回指向 x 的指针(`X | t.CPtr` 化简为 CPtr[X]) + * - `*p`:解引用,返回 p 指向的目标类型 + * - 非指针解引用报错 + * + * 以及 C 指针类型注解语法(Viper 方言): + * - `p: T*` → CPtr[T] + * - `p: T**` → CPtr[CPtr[T]] + * - `p: T***` → CPtr[CPtr[CPtr[T]]] + * + * 运行:npx jest src/tests/viperUnaryPointer.test.ts --runInBand + */ + +import assert from 'assert'; +import * as path from 'path'; + +import { ImportResolver } from '../analyzer/importResolver'; +import { Program } from '../analyzer/program'; +import { ParseTreeWalker } from '../analyzer/parseTreeWalker'; +import { TypeEvaluator } from '../analyzer/typeEvaluatorTypes'; +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 { convertOffsetToPosition } from '../common/positionUtils'; +import { FunctionNode, ParameterNode, TypeAnnotationNode, UnaryOperationNode } from '../parser/parseNodes'; +import { OperatorType } from '../parser/tokenizerTypes'; + +(global as any).__rootDirectory = path.resolve(); + +const TRANSPYC_INCLUDES = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes'; + +function buildProgramForCode(code: string) { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const projectRoot = UriEx.file(TRANSPYC_INCLUDES); + 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 fs = require('fs'); + const testDir = path.join(__dirname, '__unary_ptr__'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'unary_ptr.vp'); + fs.writeFileSync(testFile, code, 'utf8'); + + const uri = UriEx.file(testFile); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + const sourceFile = program.getSourceFile(uri); + assert(sourceFile, 'source file not found'); + const parseResults = sourceFile.getParseResults(); + assert(parseResults, 'parse results not found'); + + return { program, configOptions, parseResults, uri, testDir, serviceProvider }; +} + +function getDiagnostics(program: Program, configOptions: ConfigOptions, uri: any): string[] { + const sourceFile = program.getSourceFile(uri); + assert(sourceFile, 'source file not found'); + return (sourceFile.getDiagnostics(configOptions) || []).map((diag) => { + const line = diag.range ? diag.range.start.line + 1 : -1; + return `[category=${diag.category}][line=${line}] ${diag.message}`; + }); +} + +test('Viper 一元 * 解引用 / & 取地址 类型求值', () => { + const code = `import t, c + +class MyStruct(): + a: t.CInt + b: t.CInt + +def test_addr(x: t.CInt): + # &x 返回指向 x 的指针:CPtr[CInt] + p: t.CInt | t.CPtr = &x + y: t.CInt = *p + +def test_struct_ptr(s: MyStruct): + # &s 返回指向 MyStruct 的指针,解引用后可访问成员 + p: MyStruct | t.CPtr = &s + a: t.CInt = (*p).a + b: t.CInt = p.a + +def test_bad_deref(x: t.CInt): + # 非指针解引用:应报错 + y: t.CInt = *x +`; + const { program, configOptions, parseResults, uri, testDir, serviceProvider } = buildProgramForCode(code); + + const evaluator = program.evaluator as TypeEvaluator | undefined; + assert(evaluator, 'evaluator not available'); + const ev = evaluator; + + // 收集所有一元运算节点(* / &)的类型 + const unaryOps: { line: number; op: string; type: string }[] = []; + class UnaryCollector extends ParseTreeWalker { + override visitUnaryOperation(node: UnaryOperationNode) { + if (node.d.operator !== OperatorType.Multiply && node.d.operator !== OperatorType.BitwiseAnd) { + return true; + } + const op = node.d.operator === OperatorType.Multiply ? '*' : '&'; + const type = ev.getType(node as any); + unaryOps.push({ + line: convertOffsetToPosition(node.start, parseResults.parserOutput.lines).line + 1, + op, + type: type ? program.printType(type) : '(undefined)', + }); + return true; + } + } + new UnaryCollector().walk(parseResults.parserOutput.parseTree); + + console.log('=== 一元 * / & 节点类型 ==='); + unaryOps.forEach((u) => console.log(` L${u.line} ${u.op}: ${u.type}`)); + + // 断言 1:&x 返回 CPtr[CInt](L9 p 的 RHS) + const addrOf = unaryOps.find((u) => u.op === '&' && u.line === 9); + assert(addrOf, '&x not found (L9)'); + assert(addrOf.type.includes('CInt'), `&x type should contain CInt: ${addrOf.type}`); + + // 断言 2:*p 解引用返回 CInt(L10) + const derefL10 = unaryOps.find((u) => u.op === '*' && u.line === 10); + assert(derefL10, '*p not found (L10)'); + assert(derefL10.type.includes('CInt'), `*p should resolve to CInt: ${derefL10.type}`); + + // 断言 3:(*p).a 解引用结构体(L15) + const derefL15 = unaryOps.find((u) => u.op === '*' && u.line === 15); + assert(derefL15, '(*p) not found (L15)'); + assert(derefL15.type.includes('MyStruct'), `(*p) should be MyStruct: ${derefL15.type}`); + + // 断言 4:非指针解引用 *x 报错(L20) + const diagnostics = getDiagnostics(program, configOptions, uri); + const badDerefErrors = diagnostics.filter((d) => d.includes('line=20') && d.includes('*')); + console.log('=== L20 诊断 ==='); + diagnostics.filter((d) => d.includes('line=20')).forEach((d) => console.log(` ${d}`)); + assert(badDerefErrors.length > 0, `*x (non-pointer) should report error: ${JSON.stringify(diagnostics)}`); + + program.dispose(); + serviceProvider.dispose(); +}); + +test('Viper 指针类型注解 T* / T** / T*** 类型求值', () => { + const code = `import t, c + +class MyStruct(): + a: t.CInt + b: t.CInt + +p1: t.CInt* +p2: t.CInt** +p3: t.CInt*** + +sp: MyStruct* +v: t.CInt = sp.a + +def test_interop(x: t.CInt): + q: t.CInt* = &x + v: t.CInt = *q + +def test_double_deref(pp: t.CInt**): + v: t.CInt = *(*pp) + +def make_ptr(x: t.CInt) -> t.CInt*: + return &x +`; + const { program, configOptions, parseResults, uri, serviceProvider } = buildProgramForCode(code); + + const evaluator = program.evaluator as TypeEvaluator | undefined; + assert(evaluator, 'evaluator not available'); + const ev = evaluator; + + // 收集所有类型注解(变量注解 / 参数注解 / 返回类型)的类型 + const anns: { line: number; type: string }[] = []; + const collect = (node: any) => { + const t = ev.getTypeOfAnnotation(node); + anns.push({ + line: convertOffsetToPosition(node.start, parseResults.parserOutput.lines).line + 1, + type: t ? program.printType(t) : '(undefined)', + }); + }; + class AnnCollector extends ParseTreeWalker { + override visitTypeAnnotation(node: TypeAnnotationNode) { + collect(node.d.annotation); + return true; + } + override visitParameter(node: ParameterNode) { + if (node.d.annotation) { + collect(node.d.annotation); + } + return true; + } + override visitFunction(node: FunctionNode) { + if (node.d.returnAnnotation) { + collect(node.d.returnAnnotation); + } + return true; + } + } + new AnnCollector().walk(parseResults.parserOutput.parseTree); + + console.log('=== 指针类型注解类型 ==='); + anns.forEach((a) => console.log(` L${a.line}: ${a.type}`)); + + // T* → CPtr[CInt](L7) + const p1 = anns.find((a) => a.line === 7); + assert(p1, 'p1 annotation not found (L7)'); + assert(p1.type.includes('CPtr[CInt]'), `p1 should be CPtr[CInt]: ${p1.type}`); + + // T** → CPtr[CPtr[CInt]](L8) + const p2 = anns.find((a) => a.line === 8); + assert(p2, 'p2 annotation not found (L8)'); + assert(p2.type.includes('CPtr[CPtr[CInt]]'), `p2 should be CPtr[CPtr[CInt]]: ${p2.type}`); + + // T*** → CPtr[CPtr[CPtr[CInt]]](L9) + const p3 = anns.find((a) => a.line === 9); + assert(p3, 'p3 annotation not found (L9)'); + assert(p3.type.includes('CPtr[CPtr[CPtr[CInt]]]'), `p3 should be CPtr[CPtr[CPtr[CInt]]]: ${p3.type}`); + + // 结构体指针 MyStruct* → CPtr[MyStruct](L11) + const sp = anns.find((a) => a.line === 11); + assert(sp, 'sp annotation not found (L11)'); + assert(sp.type.includes('CPtr[MyStruct]'), `sp should be CPtr[MyStruct]: ${sp.type}`); + + // 参数 q: t.CInt*(L15) + const q = anns.find((a) => a.line === 15); + assert(q, 'q annotation not found (L15)'); + assert(q.type.includes('CPtr[CInt]'), `q should be CPtr[CInt]: ${q.type}`); + + // 参数 pp: t.CInt**(L18) + const pp = anns.find((a) => a.line === 18); + assert(pp, 'pp annotation not found (L18)'); + assert(pp.type.includes('CPtr[CPtr[CInt]]'), `pp should be CPtr[CPtr[CInt]]: ${pp.type}`); + + // 返回类型 -> t.CInt*(L21) + const ret = anns.find((a) => a.line === 21); + assert(ret, 'return annotation not found (L21)'); + assert(ret.type.includes('CPtr[CInt]'), `return should be CPtr[CInt]: ${ret.type}`); + + // 不得有任何 Error 类别诊断(指针类型注解不应产生错误) + const diagnostics = getDiagnostics(program, configOptions, uri); + const errors = diagnostics.filter((d) => d.includes('category=0')); + console.log('=== 错误诊断 ==='); + errors.forEach((d) => console.log(` ${d}`)); + assert(errors.length === 0, `pointer type annotations should produce no errors: ${JSON.stringify(errors)}`); + + program.dispose(); + serviceProvider.dispose(); +}); + +test('Viper 无空格 T* = 赋值:词法 `*=`/`**=` 拆分为指针后缀 + 赋值号', () => { + const code = `import t, c + +def main() -> int: + x: int = 42 + px: int*=&x + ppx: int**=&px + d: int = *px + return 0 +`; + const { program, configOptions, parseResults, uri, serviceProvider } = buildProgramForCode(code); + + const evaluator = program.evaluator as TypeEvaluator | undefined; + assert(evaluator, 'evaluator not available'); + const ev = evaluator; + + const anns: { line: number; type: string }[] = []; + class AnnCollector extends ParseTreeWalker { + override visitTypeAnnotation(node: TypeAnnotationNode) { + const t = ev.getTypeOfAnnotation(node.d.annotation); + anns.push({ + line: convertOffsetToPosition(node.d.annotation.start, parseResults.parserOutput.lines).line + 1, + type: t ? program.printType(t) : '(undefined)', + }); + return true; + } + } + new AnnCollector().walk(parseResults.parserOutput.parseTree); + + console.log('=== 无空格 *=/ **= 注解类型 ==='); + anns.forEach((a) => console.log(` L${a.line}: ${a.type}`)); + + // L5: px: int*=&x → CPtr[int];L6: ppx: int**=&px → CPtr[CPtr[int]] + const px = anns.find((a) => a.line === 5); + assert(px, 'px annotation not found (L5)'); + assert(px.type.includes('CPtr[int]'), `px should be CPtr[int]: ${px.type}`); + const ppx = anns.find((a) => a.line === 6); + assert(ppx, 'ppx annotation not found (L6)'); + assert(ppx.type.includes('CPtr[CPtr[int]]'), `ppx should be CPtr[CPtr[int]]: ${ppx.type}`); + + // 不得有 Error 类别诊断(含内部错误) + const diagnostics = getDiagnostics(program, configOptions, uri); + const errors = diagnostics.filter((d) => d.includes('category=0')); + console.log('=== 错误诊断 ==='); + errors.forEach((d) => console.log(` ${d}`)); + assert(errors.length === 0, `no-space star-assign should produce no errors: ${JSON.stringify(errors)}`); + + program.dispose(); + serviceProvider.dispose(); +}); + +test('Viper 解引用赋值:*p = value(联合形式与 T* 形式)', () => { + const code = `import t, c + +def main() -> int: + x: int = 42 + px: int | t.CPtr = &x + *px = 100 + y: int = *px + pz: int* = &x + *pz = 7 + return 0 +`; + const { program, configOptions, parseResults, uri, serviceProvider } = buildProgramForCode(code); + + const evaluator = program.evaluator as TypeEvaluator | undefined; + assert(evaluator, 'evaluator not available'); + const ev = evaluator; + + const anns: { line: number; type: string }[] = []; + class AnnCollector extends ParseTreeWalker { + override visitTypeAnnotation(node: TypeAnnotationNode) { + const t = ev.getTypeOfAnnotation(node.d.annotation); + anns.push({ + line: convertOffsetToPosition(node.d.annotation.start, parseResults.parserOutput.lines).line + 1, + type: t ? program.printType(t) : '(undefined)', + }); + return true; + } + } + new AnnCollector().walk(parseResults.parserOutput.parseTree); + + console.log('=== 解引用赋值 注解类型 ==='); + anns.forEach((a) => console.log(` L${a.line}: ${a.type}`)); + + // L5: px: int | t.CPtr → CPtr[int](联合折叠);L8: pz: int* → CPtr[int] + const px = anns.find((a) => a.line === 5); + assert(px, 'px annotation not found (L5)'); + assert(px.type.includes('CPtr[int]'), `px should fold to CPtr[int]: ${px.type}`); + const pz = anns.find((a) => a.line === 8); + assert(pz, 'pz annotation not found (L8)'); + assert(pz.type.includes('CPtr[int]'), `pz should be CPtr[int]: ${pz.type}`); + + // *px = 100(L6)与 *pz = 7(L8)不得报"表达式不能是赋值目标"; + // y: int = *px 解引用右值赋值兼容。 + const diagnostics = getDiagnostics(program, configOptions, uri); + const errors = diagnostics.filter((d) => d.includes('category=0')); + console.log('=== 错误诊断 ==='); + errors.forEach((d) => console.log(` ${d}`)); + assert(errors.length === 0, `deref assignment should produce no errors: ${JSON.stringify(errors)}`); + assert( + !diagnostics.some((d) => d.includes('赋值目标')), + `deref should not report assignment target error: ${JSON.stringify(diagnostics)}` + ); + + program.dispose(); + serviceProvider.dispose(); +}); diff --git a/packages/vipeye-internal/src/tests/viperUnknownMemberAccess.test.ts b/packages/vipeye-internal/src/tests/viperUnknownMemberAccess.test.ts new file mode 100644 index 00000000..8c3dc853 --- /dev/null +++ b/packages/vipeye-internal/src/tests/viperUnknownMemberAccess.test.ts @@ -0,0 +1,123 @@ +/* + * viperUnknownMemberAccess.test.ts + * + * 验证 Viper 的"对象可能未定义"(viperUnknownMemberAccess)诊断: + * - 仅对"最终" Unknown(isIncomplete=false,如 import 解析失败)报错; + * - 对分析中间态 Unknown(isIncomplete=true,如循环 import 中方法 self + * 绑定尚未收敛)不报错,避免误报。 + * + * 回归场景:TransPyC StructGenMixin.get_mro_chain(L1358)内 `self.find_struct(cur)`, + * 其 self 参数在循环 import(StructGenMixin ⇄ Generator)分析早期为 + * Unknown(incomplete),收敛后为 Self@StructGenMixin。含 App 搜索根时不得报错; + * 缺 App 搜索根(Generator.vp import 失败)时 `self.Gen.StructGen.find_struct` + * 应正常报"对象可能未定义"。 + * + * 运行:npx jest src/tests/viperUnknownMemberAccess.test.ts --runInBand + */ + +import assert from 'assert'; +import * as path from 'path'; + +import { ImportResolver } from '../analyzer/importResolver'; +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 { Uri } from '../common/uri/uri'; + +(global as any).__rootDirectory = path.resolve(); + +const TRANSPYC_INCLUDES = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes'; +const TPV_APP = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\TransPyV\\App'; +const STRUCT_GEN_MIXIN = + 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\TransPyV\\App\\lib\\core\\Gen\\StructGenMixin.vp'; +const ASSIGN_GEN_MIXIN = + 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\TransPyV\\App\\lib\\core\\Gen\\AssignGenMixin.vp'; + +function buildProgram(targetPath: string, extraPaths: string[]) { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const projectRoot = UriEx.file(path.dirname(targetPath)); + const configOptions = new ConfigOptions(projectRoot); + configOptions.internalTestMode = true; + configOptions.initializeTypeCheckingMode('standard'); + + const execEnv = new ExecutionEnvironment( + 'python', + projectRoot, + configOptions.diagnosticRuleSet, + configOptions.defaultPythonVersion, + configOptions.defaultPythonPlatform, + [projectRoot, ...extraPaths.map((p) => UriEx.file(p))], + /* 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(targetPath); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + return { program, configOptions, uri, serviceProvider }; +} + +function getFindStructDiags(program: Program, configOptions: ConfigOptions, uri: any): string[] { + const sourceFile = program.getSourceFile(uri); + assert(sourceFile, 'source file not found'); + return (sourceFile.getDiagnostics(configOptions) || []) + .filter((d) => d.message.includes('find_struct')) + .map((d) => { + const line = d.range ? d.range.start.line + 1 : -1; + return `[category=${d.category}][line=${line}] ${d.message}`; + }); +} + +test('循环 import 中间态 Unknown(isIncomplete)不报"对象可能未定义"', () => { + // 含 App 搜索根:Generator.vp import 全部解析成功,self 收敛为 Self@StructGenMixin, + // get_mro_chain 内 self.find_struct(L1358)不得误报。 + const { program, configOptions, uri, serviceProvider } = buildProgram(STRUCT_GEN_MIXIN, [ + TRANSPYC_INCLUDES, + TPV_APP, + ]); + + const findStructDiags = getFindStructDiags(program, configOptions, uri); + console.log('=== 含 App 根: find_struct 诊断 ==='); + findStructDiags.forEach((d) => console.log(` ${d}`)); + assert.strictEqual( + findStructDiags.length, + 0, + `self.find_struct should not report on incomplete Unknown: ${JSON.stringify(findStructDiags)}` + ); + + program.dispose(); + serviceProvider.dispose(); +}); + +test('import 解析失败的最终 Unknown 仍报"对象可能未定义"', () => { + // 缺 App 搜索根:Generator.vp 的 lib.core.* import 失败 → self.Gen 链为最终 + // Unknown(isIncomplete=false)→ self.Gen.StructGen.find_struct 应报错(真实问题)。 + const { program, configOptions, uri, serviceProvider } = buildProgram(ASSIGN_GEN_MIXIN, [TRANSPYC_INCLUDES]); + + const findStructDiags = getFindStructDiags(program, configOptions, uri); + console.log('=== 缺 App 根: find_struct 诊断 ==='); + findStructDiags.forEach((d) => console.log(` ${d}`)); + assert( + findStructDiags.length >= 3, + `final Unknown should report find_struct errors: ${JSON.stringify(findStructDiags)}` + ); + assert( + findStructDiags.every((d) => d.includes('对象可能未定义')), + `find_struct errors should be viperUnknownMemberAccess: ${JSON.stringify(findStructDiags)}` + ); + + program.dispose(); + serviceProvider.dispose(); +}); diff --git a/packages/vipeye-internal/src/tests/viperVpnci.test.ts b/packages/vipeye-internal/src/tests/viperVpnci.test.ts new file mode 100644 index 00000000..157105c1 --- /dev/null +++ b/packages/vipeye-internal/src/tests/viperVpnci.test.ts @@ -0,0 +1,108 @@ +/* + * viperVpnci.test.ts + * + * 验证 Viper 不可编译注解文件 `.vpnci` 与 `.vpi` 配置完全一致: + * - `import MyModule` 可解析到 MyModule.vpnci(不报"导入失败/无法解析") + * - .vpnci 文件被识别为 stub 文件(isStubFile) + * - from 导入其中的声明可正常引用 + * + * 运行:npx jest src/tests/viperVpnci.test.ts --runInBand + */ + +import assert from 'assert'; +import * as path from 'path'; + +import { ImportResolver } from '../analyzer/importResolver'; +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'; + +(global as any).__rootDirectory = path.resolve(); + +function buildProgram(files: Record) { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const fs = require('fs'); + const testRoot = path.join(__dirname, '__vpnci__'); + fs.rmSync(testRoot, { recursive: true, force: true }); + for (const [relPath, content] of Object.entries(files)) { + const absPath = path.join(testRoot, relPath); + fs.mkdirSync(path.dirname(absPath), { recursive: true }); + fs.writeFileSync(absPath, content, '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 uris = Object.keys(files).map((relPath) => UriEx.file(path.join(testRoot, relPath))); + const urisMap: Record = {}; + for (const relPath of Object.keys(files)) { + urisMap[relPath] = UriEx.file(path.join(testRoot, relPath)); + } + program.setTrackedFiles(uris); + while (program.analyze()) { + // 循环 + } + + return { program, configOptions, urisMap, testRoot, serviceProvider }; +} + +function getDiagnostics(program: Program, configOptions: ConfigOptions, uri: any): string[] { + const sourceFile = program.getSourceFile(uri); + assert(sourceFile, 'source file not found'); + return (sourceFile.getDiagnostics(configOptions) || []).map((diag) => { + const line = diag.range ? diag.range.start.line + 1 : -1; + return `[category=${diag.category}][line=${line}] ${diag.message}`; + }); +} + +test('.vpnci 不可编译注解文件:import 解析成功、识别为 stub、声明可引用', () => { + const { program, configOptions, urisMap, testRoot, serviceProvider } = buildProgram({ + 'MyModule.vpnci': `def helper() -> int: + ... +`, + 'main.vp': `from MyModule import helper +import MyModule + +def main() -> int: + return helper() + MyModule.helper() +`, + }); + + // 1. main.vp 中 import/from 均解析成功,无"无法解析"或未定义 + const mainDiags = getDiagnostics(program, configOptions, urisMap['main.vp']); + assert( + !mainDiags.some((d) => d.includes('无法解析') || d.includes('could not be resolved') || d.includes('未定义')), + `import MyModule 应解析成功且 helper 可引用,实际: ${JSON.stringify(mainDiags)}` + ); + + // 2. .vpnci 文件被识别为 stub 文件(与 .vpi 一致) + const stubFile = program.getSourceFile(urisMap['MyModule.vpnci']); + assert(stubFile, 'MyModule.vpnci 应被程序索引'); + assert(stubFile.isStubFile(), 'MyModule.vpnci 应被识别为 stub 文件'); + + program.dispose(); + serviceProvider.dispose(); + require('fs').rmSync(testRoot, { recursive: true, force: true }); +}); diff --git a/packages/vipeye-internal/typeshed-fallback/stdlib/builtins.vpi b/packages/vipeye-internal/typeshed-fallback/stdlib/builtins.vpi index d3ba7920..1586efe1 100644 --- a/packages/vipeye-internal/typeshed-fallback/stdlib/builtins.vpi +++ b/packages/vipeye-internal/typeshed-fallback/stdlib/builtins.vpi @@ -2,6 +2,7 @@ import _ast import _sitebuiltins import _typeshed import sys +import t import types from _collections_abc import dict_items, dict_keys, dict_values from _typeshed import ( @@ -2661,4 +2662,13 @@ def LInp(expr: Any, constraint: Any = None) -> Any: def LOut(expr: Any, constraint: Any = None) -> Any: """LLVM IR 输出操作数标记(等价 c.LOut)。""" - ... \ No newline at end of file + ... + +# --- Viper 扩展:编译器内置裸指针类型 rowptr --- +# rowptr 是编译器内置类型(TPC/TPV 双端,语法保留字,不可重定义): +# rowptr[T] 固定表示"裸指针,指向原始类型 T"(等价 t.CPtr[T]), +# 单独 rowptr 等价 t.CPtr(void*)。 +# 声明在 builtins stub 中使 LSP 对任意 Viper 工程均可解析 rowptr,不依赖 +# 项目 includes/builtins.vp 是否声明;编译器侧解析 rowptr 时优先走内置 +# 路径并忽略本声明(见 TransPyC TypeAnnotationResolver / SymbolExtractor)。 +rowptr: t.CTypedef = t.CPtr \ No newline at end of file diff --git a/packages/vscode-vipeye/package.json b/packages/vscode-vipeye/package.json index ac1ee0f0..7bbeeb4a 100644 --- a/packages/vscode-vipeye/package.json +++ b/packages/vscode-vipeye/package.json @@ -38,7 +38,8 @@ ], "extensions": [ ".vp", - ".vpi" + ".vpi", + ".vpnci" ], "configuration": "./language-configuration.json", "icon": { @@ -152,6 +153,9 @@ "entity.name.function.viper", "entity.name.function" ], + "macro": [ + "storage.type.function.viper" + ], "class": [ "entity.name.class", "entity.name.type" diff --git a/packages/vscode-vipeye/syntaxes/viper.tmLanguage.json b/packages/vscode-vipeye/syntaxes/viper.tmLanguage.json index 3b8cec5b..466f9aa9 100644 --- a/packages/vscode-vipeye/syntaxes/viper.tmLanguage.json +++ b/packages/vscode-vipeye/syntaxes/viper.tmLanguage.json @@ -31,7 +31,7 @@ "patterns": [ { "name": "keyword.control.flow.viper", - "match": "\\b(if|elif|else|while|for|in|return|break|continue|pass|raise|try|except|finally|with|as|import|from|yield|async|await|del|assert|lambda|match|case)\\b" + "match": "\\b(if|elif|else|while|for|in|return|break|continue|pass|raise|try|except|finally|with|as|import|from|yield|async|await|del|assert|lambda|match|case|define)\\b" }, { "name": "storage.type.function.viper",