From 525a654f5c18caa7e8e5e733eb2ea495533102f2 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Mon, 21 Sep 2026 23:13:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(vipeye-internal):=20=E6=94=AF=E6=8C=81=20e?= =?UTF-8?q?num=20X:=20=E6=9E=9A=E4=B8=BE=E8=AF=AD=E6=B3=95=E4=B8=8E=20defi?= =?UTF-8?q?ne=20=E5=B8=B8=E9=87=8F=E5=A3=B0=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - enum:`enum NAME:` 识别为 Viper 枚举(等价 class NAME(t.CEnum)), 成员支持裸名与逗号分隔(enum X: a, b, c),并支持同行声明后换行 TAB 续写 - enum:取值按编译期规则推导(从 0 递增,中间显式赋值后从该值 +1 续推), hover 显示 (variable) IsAsync: ASTFlag = 0,成员可隐式转换为 int - enum:成员推导值超出声明类型位宽时报错(如 i1 存不下 2) - define:`define NAME: 类型 = 值` / `define NAME = 值` 解析为常量声明, `define f(...)` 宏函数行为不变 - 新增整数字面量位宽校验:字面量无法存入声明类型时提示扩容, define 与普通声明均生效(位宽表源自 wiki 02-type-system.md) - 新增 viperEnumFlag.test.ts(5 项)与 viperDefineConst.test.ts(4 项) --- .../vipeye-internal/src/analyzer/binder.ts | 54 +++ .../vipeye-internal/src/analyzer/checker.ts | 277 +++++++++++++++- .../vipeye-internal/src/analyzer/enums.ts | 119 ++++++- .../src/analyzer/typeEvaluator.ts | 12 +- .../vipeye-internal/src/analyzer/types.ts | 10 + .../src/languageService/hoverProvider.ts | 26 ++ .../src/localization/localize.ts | 8 + .../src/localization/package.nls.en-us.json | 8 + .../src/localization/package.nls.zh-cn.json | 2 + .../vipeye-internal/src/parser/parseNodes.ts | 6 + packages/vipeye-internal/src/parser/parser.ts | 72 +++- .../vipeye-internal/src/parser/tokenizer.ts | 34 +- .../src/parser/tokenizerTypes.ts | 10 +- .../tests/__define_const__/define_const_0.vp | 12 + .../tests/__define_const__/define_const_1.vp | 5 + .../tests/__define_const__/define_const_2.vp | 11 + .../tests/__define_const__/define_const_3.vp | 9 + .../src/tests/__enum_flag__/enum_flag_0.vp | 19 ++ .../src/tests/__enum_flag__/enum_flag_1.vp | 12 + .../src/tests/__enum_flag__/enum_flag_2.vp | 8 + .../src/tests/__enum_flag__/enum_flag_3.vp | 10 + .../src/tests/__enum_flag__/enum_flag_4.vp | 10 + .../src/tests/viperDefineConst.test.ts | 243 ++++++++++++++ .../src/tests/viperEnumFlag.test.ts | 311 ++++++++++++++++++ 24 files changed, 1272 insertions(+), 16 deletions(-) create mode 100644 packages/vipeye-internal/src/tests/__define_const__/define_const_0.vp create mode 100644 packages/vipeye-internal/src/tests/__define_const__/define_const_1.vp create mode 100644 packages/vipeye-internal/src/tests/__define_const__/define_const_2.vp create mode 100644 packages/vipeye-internal/src/tests/__define_const__/define_const_3.vp create mode 100644 packages/vipeye-internal/src/tests/__enum_flag__/enum_flag_0.vp create mode 100644 packages/vipeye-internal/src/tests/__enum_flag__/enum_flag_1.vp create mode 100644 packages/vipeye-internal/src/tests/__enum_flag__/enum_flag_2.vp create mode 100644 packages/vipeye-internal/src/tests/__enum_flag__/enum_flag_3.vp create mode 100644 packages/vipeye-internal/src/tests/__enum_flag__/enum_flag_4.vp create mode 100644 packages/vipeye-internal/src/tests/viperDefineConst.test.ts create mode 100644 packages/vipeye-internal/src/tests/viperEnumFlag.test.ts diff --git a/packages/vipeye-internal/src/analyzer/binder.ts b/packages/vipeye-internal/src/analyzer/binder.ts index 9692fce4..b98bd59b 100644 --- a/packages/vipeye-internal/src/analyzer/binder.ts +++ b/packages/vipeye-internal/src/analyzer/binder.ts @@ -547,6 +547,13 @@ export class Binder extends ParseTreeWalker { this._dunderSlotsEntries = undefined; if (!this._moduleSymbolOnly) { + // Viper 扩展:`enum X:` 的成员允许只写成员名(裸名 / 逗号分隔的裸名), + // 这不是赋值语句,Python 规则下不会产生声明。 + // 这里先补出类作用域声明,使成员访问、语义高亮与枚举语义可用。 + if (node.d.isViperEnum) { + this._bindViperEnumMemberNames(node.d.suite); + } + // Analyze the suite. this.walk(node.d.suite); } @@ -3772,6 +3779,53 @@ export class Binder extends ParseTreeWalker { return undefined; } + // Viper 扩展:`enum X:` 的成员允许只写成员名: + // enum ASTFlag: + // IsAsync + // Simple + // HasStar: int + // enum X: a, b, c + // 裸名(含逗号分隔的裸名元组)在 Python 规则下只是表达式语句,不产生声明, + // 这里为它们补出类作用域声明;取值由编译期推导(见 checker 的枚举取值校验)。 + private _bindViperEnumMemberNames(suite: SuiteNode): void { + suite.d.statements.forEach((statement) => { + if (statement.nodeType !== ParseNodeType.StatementList) { + return; + } + + statement.d.statements.forEach((subStatement) => { + this._bindViperEnumMemberNameNode(subStatement); + }); + }); + } + + private _bindViperEnumMemberNameNode(node: ParseNode): void { + if (node.nodeType === ParseNodeType.Name) { + this._addViperEnumMemberDeclaration(node); + } else if (node.nodeType === ParseNodeType.Tuple) { + node.d.items.forEach((item) => this._bindViperEnumMemberNameNode(item)); + } + } + + private _addViperEnumMemberDeclaration(node: NameNode): void { + const symbol = this._bindNameToScope(this._currentScope, node); + if (!symbol) { + return; + } + + const declaration: VariableDeclaration = { + type: DeclarationType.Variable, + node, + isConstant: isConstantName(node.d.value), + uri: this._fileInfo.fileUri, + range: convertTextRangeToRange(node, this._fileInfo.lines), + moduleName: this._fileInfo.moduleName, + isInExceptSuite: this._isInExceptSuite, + isExplicitBinding: this._currentScope.getBindingType(node.d.value) !== undefined, + }; + symbol.addDeclaration(declaration); + } + private _bindPossibleTupleNamedTarget(target: ExpressionNode, addedSymbols?: Map) { switch (target.nodeType) { case ParseNodeType.Name: { diff --git a/packages/vipeye-internal/src/analyzer/checker.ts b/packages/vipeye-internal/src/analyzer/checker.ts index 91c270f8..827486a1 100644 --- a/packages/vipeye-internal/src/analyzer/checker.ts +++ b/packages/vipeye-internal/src/analyzer/checker.ts @@ -99,7 +99,13 @@ import { addInheritedDataClassEntries } from './dataClasses'; import { Declaration, DeclarationType, isAliasDeclaration, isVariableDeclaration } from './declaration'; import { getNameNodeForDeclaration } from './declarationUtils'; import { getDeprecatedAliases, getDeprecatedSpecialForms } from './deprecatedSymbols'; -import { getEnumDeclaredValueType, isEnumClassWithMembers, transformTypeForEnumMember } from './enums'; +import { + getEnumDeclaredValueType, + getViperEnumMemberValues, + getViperIntegerLiteralValue, + isEnumClassWithMembers, + transformTypeForEnumMember, +} from './enums'; import { ImportResolver, createImportedModuleDescriptor } from './importResolver'; import { ImportResult, ImportType } from './importResult'; import { getRelativeModuleName, getTopLevelImports } from './importStatementUtils'; @@ -411,6 +417,11 @@ export class Checker extends ParseTreeWalker { this._validateEnumMembers(classTypeResult.classType, node); + // Viper 扩展:`enum X:` 枚举成员的编译期推取值需要能存入成员声明的类型。 + if (node.d.isViperEnum) { + this._validateViperEnumMemberValues(node); + } + if (ClassType.isTypedDictClass(classTypeResult.classType)) { this._validateTypedDictClassSuite(node.d.suite); } @@ -1234,6 +1245,9 @@ export class Checker extends ParseTreeWalker { override visitAssignment(node: AssignmentNode): boolean { this._evaluator.evaluateTypesForStatement(node); + // Viper 扩展:整数字面量必须能存入声明类型的位宽(如 int 为 i32)。 + this._validateViperIntegerLiteralType(node); + if (node.d.annotationComment) { this._evaluator.getType(node.d.annotationComment); @@ -2018,6 +2032,12 @@ export class Checker extends ParseTreeWalker { return; } + // Viper 扩展:`enum X:` 的成员可以只写成员名(裸名 / 逗号分隔的裸名), + // 它们是成员声明而不是"无效果的表达式",不报未使用表达式。 + if (this._isViperEnumMemberNameExpression(node)) { + return; + } + const simpleExpressionTypes = [ ParseNodeType.UnaryOperation, ParseNodeType.BinaryOperation, @@ -2060,6 +2080,144 @@ export class Checker extends ParseTreeWalker { } } + // Viper 扩展:判断表达式语句是否为 `enum X:` 的成员名声明 + // (类体内单独的裸名语句,或逗号分隔的裸名元组)。 + private _isViperEnumMemberNameExpression(node: ParseNode): boolean { + if (node.nodeType === ParseNodeType.Name) { + return this._isViperEnumMemberNameNode(node); + } + + if ( + node.nodeType === ParseNodeType.Tuple && + node.d.items.length > 0 && + node.d.items.every((item) => item.nodeType === ParseNodeType.Name) + ) { + // 逗号分隔的裸名成员(enum X: a, b, c)。 + return ParseTreeUtils.getEnclosingClass(node, /* stopAtFunction */ true)?.d.isViperEnum === true; + } + + return false; + } + + // Viper 扩展:判断裸名节点是否直接作为 `enum X:` 类体内的成员声明语句出现。 + private _isViperEnumMemberNameNode(node: NameNode): boolean { + const parent = node.parent; + const isMemberStatement = + parent?.nodeType === ParseNodeType.StatementList || + (parent?.nodeType === ParseNodeType.Tuple && parent.parent?.nodeType === ParseNodeType.StatementList); + + return ( + isMemberStatement && + ParseTreeUtils.getEnclosingClass(node, /* stopAtFunction */ true)?.d.isViperEnum === true + ); + } + + // Viper 扩展:校验 `enum X:` 枚举成员的编译期推导取值能否存入成员声明的类型。 + // 取值从 0 递增推导(见 enums.ts),故按非负取值给出范围(i1 即 0 ~ 1)。 + // 只处理"仅注解无赋值"的成员:带显式赋值的成员(如 `c: t.i1 = 2`)由 + // _validateViperIntegerLiteralType 按整数字面量规则统一校验,避免重复报错。 + private _validateViperEnumMemberValues(node: ClassNode) { + const memberValues = getViperEnumMemberValues(node); + + node.d.suite.d.statements.forEach((statement) => { + if (statement.nodeType !== ParseNodeType.StatementList) { + return; + } + + statement.d.statements.forEach((subStatement) => { + // `成员: 类型`(无显式赋值):取值由编译期推导。 + if ( + subStatement.nodeType !== ParseNodeType.TypeAnnotation || + subStatement.d.valueExpr.nodeType !== ParseNodeType.Name + ) { + return; + } + + const nameNode = subStatement.d.valueExpr; + const value = memberValues.get(nameNode.d.value); + if (value === undefined) { + return; + } + + const range = this._getViperIntegerTypeRange( + subStatement.d.annotation, + /* allowNegativeBitVector */ false + ); + if (!range || (value >= range.min && value <= range.max)) { + return; + } + + this._evaluator.addDiagnostic( + DiagnosticRule.reportGeneralTypeIssues, + LocMessage.viperEnumValueOutOfRange().format({ + value: value.toString(), + type: range.typeName, + min: range.min.toString(), + max: range.max.toString(), + }), + nameNode + ); + }); + }); + } + + // Viper 扩展:取枚举成员类型注解对应的整型可表示范围。 + // 优先使用注解书写形式(如 `i1` / `t.CInt8T`)解析,别名形式再回退到已解析类型名。 + private _getViperIntegerTypeRange( + declaredTypeNode: ExpressionNode, + allowNegativeBitVector: boolean + ): ViperIntegerRange | undefined { + const writtenName = getViperWrittenTypeName(declaredTypeNode); + if (writtenName) { + const range = getViperIntegerTypeRangeFromName(writtenName, allowNegativeBitVector); + if (range) { + return range; + } + } + + const annotationType = this._evaluator.getTypeOfAnnotation(declaredTypeNode); + if (annotationType && isClass(annotationType) && annotationType.shared.name) { + const range = getViperIntegerTypeRangeFromName(annotationType.shared.name, allowNegativeBitVector); + if (range) { + return range; + } + } + + return undefined; + } + + // Viper 扩展:`NAME: 类型 = 整数字面量` 时校验字面量能否存入类型的位宽。 + // 例如 `define CONST_INT: int = 10000000000000000000000000000000` + // (int 为 i32,无法正确存储该数字,需要扩容);`define` 与普通声明形式同样适用。 + private _validateViperIntegerLiteralType(node: AssignmentNode) { + const leftExpr = node.d.leftExpr; + if (leftExpr.nodeType !== ParseNodeType.TypeAnnotation) { + return; + } + + const literalValue = getViperIntegerLiteralValue(node.d.rightExpr); + if (literalValue === undefined) { + return; + } + + const range = this._getViperIntegerTypeRange(leftExpr.d.annotation, /* allowNegativeBitVector */ true); + if (!range || (literalValue >= range.min && literalValue <= range.max)) { + return; + } + + this._evaluator.addDiagnostic( + DiagnosticRule.reportGeneralTypeIssues, + LocMessage.viperIntegerLiteralOutOfRange().format({ + // 用源码书写形式,超长字面量不丢精度(如 10^31)。 + value: ParseTreeUtils.printExpression(node.d.rightExpr), + type: range.typeName, + min: range.min.toString(), + max: range.max.toString(), + }), + node.d.rightExpr + ); + } + // Verifies that the target of a nonlocal statement is not a PEP 695-style // TypeParameter. This situation results in a runtime exception. private _validateNonlocalTypeParam(node: NameNode) { @@ -4508,6 +4666,12 @@ export class Checker extends ParseTreeWalker { return; } + // Viper 扩展:`enum X:` 的裸名成员是成员声明(取值由编译期推导), + // 不是在读取一个未绑定的变量。 + if (this._isViperEnumMemberNameNode(node)) { + return; + } + // Skip this for keyword argument names. if (node.parent?.nodeType === ParseNodeType.Argument && node.parent.d.name === node) { return; @@ -7883,3 +8047,114 @@ export class Checker extends ParseTreeWalker { }); } } + +// Viper 扩展:整型的可表示范围(用于整数字面量 / 枚举取值的位宽校验)。 +interface ViperIntegerRange { + typeName: string; + min: bigint; + max: bigint; +} + +// Viper 扩展:取类型注解的书写形式(如 `i1`、`t.CInt8T` 取 `CInt8T`)。 +function getViperWrittenTypeName(declaredTypeNode: ExpressionNode): string | undefined { + if (declaredTypeNode.nodeType === ParseNodeType.Name) { + return declaredTypeNode.d.value; + } + if (declaredTypeNode.nodeType === ParseNodeType.MemberAccess) { + return declaredTypeNode.d.member.d.value; + } + return undefined; +} + +function viperSignedRange(typeName: string, bits: number): ViperIntegerRange | undefined { + if (!Number.isInteger(bits) || bits < 1 || bits > 64) { + return undefined; + } + + // 目标 ES 版本低于 ES2020,不能用 BigInt 字面量(1n),统一用 BigInt()。 + const half = BigInt(1) << BigInt(bits - 1); + return { typeName, min: -half, max: half - BigInt(1) }; +} + +function viperUnsignedRange(typeName: string, bits: number): ViperIntegerRange | undefined { + if (!Number.isInteger(bits) || bits < 1 || bits > 64) { + return undefined; + } + + return { typeName, min: BigInt(0), max: (BigInt(1) << BigInt(bits)) - BigInt(1) }; +} + +// Viper 扩展:按类型名给出可表示范围。仅覆盖宽度明确的整型,其余返回 undefined +// (不做校验,避免误报)。位宽来源见 wiki 02-type-system.md。 +// allowNegativeBitVector:LLVM 位向量短名(iN)是否允许负数——位向量的符号由运算决定, +// 枚举成员取值必然非负(从 0 递增),传 false 时范围即 [0, 2^N-1]; +// 一般整数字面量可为负,传 true 时取有/无符号并集 [-2^(N-1), 2^N-1]。 +function getViperIntegerTypeRangeFromName( + typeName: string, + allowNegativeBitVector: boolean +): ViperIntegerRange | undefined { + let match = /^u(\d+)$/.exec(typeName); + if (match) { + return viperUnsignedRange(typeName, Number(match[1])); + } + + match = /^i(\d+)$/.exec(typeName); + if (match) { + const bits = Number(match[1]); + const unsignedRange = viperUnsignedRange(typeName, bits); + if (!allowNegativeBitVector || !unsignedRange) { + return unsignedRange; + } + + const signedRange = viperSignedRange(typeName, bits); + return signedRange ? { typeName, min: signedRange.min, max: unsignedRange.max } : unsignedRange; + } + + // 定宽整数:CInt{n}T / CUInt{n}T / CInt{Least|Fast}{n}T / CIntMaxT / CUIntMaxT。 + match = /^CIntMaxT$/.exec(typeName); + if (match) { + return viperSignedRange(typeName, 64); + } + + match = /^CUIntMaxT$/.exec(typeName); + if (match) { + return viperUnsignedRange(typeName, 64); + } + + match = /^CInt(?:Least|Fast)?(\d+)T$/.exec(typeName); + if (match) { + return viperSignedRange(typeName, Number(match[1])); + } + + match = /^CUInt(?:Least|Fast)?(\d+)T$/.exec(typeName); + if (match) { + return viperUnsignedRange(typeName, Number(match[1])); + } + + // 基本 C 类型与内置类型。[位宽, 是否有符号] + const basicRanges: Record = { + CChar: [8, true], + CSignedChar: [8, true], + CUnsignedChar: [8, false], + CShort: [16, true], + CUnsignedShort: [16, false], + CInt: [32, true], + CUnsigned: [32, false], + CUnsignedInt: [32, false], + CLong: [64, true], + CUnsignedLong: [64, false], + CChar8T: [8, false], + CChar16T: [16, false], + CChar32T: [32, false], + bool: [1, false], + CBool: [1, false], + int: [32, true], + }; + + const basicRange = basicRanges[typeName]; + if (basicRange) { + return basicRange[1] ? viperSignedRange(typeName, basicRange[0]) : viperUnsignedRange(typeName, basicRange[0]); + } + + return undefined; +} diff --git a/packages/vipeye-internal/src/analyzer/enums.ts b/packages/vipeye-internal/src/analyzer/enums.ts index 7d649422..e81b27cc 100644 --- a/packages/vipeye-internal/src/analyzer/enums.ts +++ b/packages/vipeye-internal/src/analyzer/enums.ts @@ -10,7 +10,8 @@ import { assert } from '../common/debug'; import { IsViperDialect } from '../common/dialect'; import { PythonVersion, pythonVersion3_13 } from '../common/pythonVersion'; -import { ArgCategory, ExpressionNode, NameNode, ParseNode, ParseNodeType } from '../parser/parseNodes'; +import { ArgCategory, ClassNode, ExpressionNode, NameNode, ParseNode, ParseNodeType } from '../parser/parseNodes'; +import { OperatorType } from '../parser/tokenizerTypes'; import { getFileInfo } from './analyzerNodeInfo'; import { VariableDeclaration } from './declaration'; import { getClassFullName, getEnclosingClass, getTypeSourceId } from './parseTreeUtils'; @@ -301,6 +302,118 @@ export function createEnumType( return classType; } +// Viper 扩展:按声明顺序推导 `enum X:` 各成员的编译期取值。 +// 规则:从 0 递增;中间显式赋值则改用该值并从其 +1 继续; +// 显式赋的不是整数字面量时无法推导,其后成员也不推导(直到再次遇到显式整数字面量)。 +// 供枚举成员 hover 显示与取值越界校验共用同一份规则。 +export function getViperEnumMemberValues(classNode: ClassNode): Map { + const values = new Map(); + // 目标 ES 版本低于 ES2020,不能用 BigInt 字面量(0n / 1n),统一用 BigInt()。 + let nextValue: bigint | undefined = BigInt(0); + + const addMember = (nameNode: NameNode, explicitValueNode: ExpressionNode | undefined) => { + const value = explicitValueNode ? getViperIntegerLiteralValue(explicitValueNode) : nextValue; + nextValue = value === undefined ? undefined : value + BigInt(1); + + if (value !== undefined) { + values.set(nameNode.d.value, value); + } + }; + + classNode.d.suite.d.statements.forEach((statement) => { + if (statement.nodeType !== ParseNodeType.StatementList) { + return; + } + + statement.d.statements.forEach((subStatement) => { + switch (subStatement.nodeType) { + case ParseNodeType.Name: + // 裸名成员。 + addMember(subStatement, undefined); + break; + + case ParseNodeType.Tuple: + // 逗号分隔的裸名成员(enum X: a, b, c)。 + subStatement.d.items.forEach((item) => { + if (item.nodeType === ParseNodeType.Name) { + addMember(item, undefined); + } + }); + break; + + case ParseNodeType.TypeAnnotation: + // `成员: 类型`(无显式赋值)。 + if (subStatement.d.valueExpr.nodeType === ParseNodeType.Name) { + addMember(subStatement.d.valueExpr, undefined); + } + break; + + case ParseNodeType.Assignment: { + // `成员 = 值` 或 `成员: 类型 = 值`。 + const leftExpr = subStatement.d.leftExpr; + if ( + leftExpr.nodeType === ParseNodeType.TypeAnnotation && + leftExpr.d.valueExpr.nodeType === ParseNodeType.Name + ) { + addMember(leftExpr.d.valueExpr, subStatement.d.rightExpr); + } else if (leftExpr.nodeType === ParseNodeType.Name) { + addMember(leftExpr, subStatement.d.rightExpr); + } + break; + } + } + }); + }); + + return values; +} + +// Viper 扩展:从字面量表达式取整数值(支持一元负号),非整数字面量返回 undefined。 +// 用 bigint 返回,超长整数字面量(tokenizer 已按 bigint 保存)不会丢精度。 +export function getViperIntegerLiteralValue(node: ExpressionNode): bigint | undefined { + if (node.nodeType === ParseNodeType.Number) { + if (node.d.isImaginary || !node.d.isInteger) { + return undefined; + } + + if (typeof node.d.value === 'bigint') { + return node.d.value; + } + + return Number.isInteger(node.d.value) ? BigInt(node.d.value) : undefined; + } + + if (node.nodeType === ParseNodeType.UnaryOperation && node.d.operator === OperatorType.Subtract) { + const innerValue = getViperIntegerLiteralValue(node.d.expr); + return innerValue === undefined ? undefined : -innerValue; + } + + return undefined; +} + +// Viper 扩展:判断裸名节点是否为 `enum X:` 的成员声明。 +// 形如: +// enum ASTFlag: +// IsAsync —— 类体内单独的裸名语句 +// enum X: a, b, c —— 逗号分隔的裸名元组(每个裸名都是成员) +function isViperEnumBareMemberName(nameNode: NameNode): boolean { + const parent = nameNode.parent; + if (!parent) { + return false; + } + + if (parent.nodeType === ParseNodeType.StatementList) { + // 类体内单独的裸名语句(`IsAsync`)。 + } else if (parent.nodeType === ParseNodeType.Tuple && parent.parent?.nodeType === ParseNodeType.StatementList) { + // 逗号分隔的裸名(`enum X: a, b, c`)。 + } else { + return false; + } + + // 仅 `enum X:` 语法声明的枚举适用:普通类体内的裸名语句仍是普通表达式。 + return getEnclosingClass(nameNode, /* stopAtFunction */ true)?.d.isViperEnum === true; +} + // Performs the "magic" that the Enum metaclass does at runtime when it // transforms a value into an enum instance. If the specified name isn't // an enum member, this function returns undefined indicating that the @@ -399,6 +512,10 @@ export function transformTypeForEnumMember( ) { valueTypeExprNode = nameNode.parent.parent.d.rightExpr; } + } else if (IsViperDialect(getFileInfo(nameNode).executionEnvironment.dialect) && isViperEnumBareMemberName(nameNode)) { + // Viper 扩展:`enum X:` 只写成员名(裸名 / 逗号分隔的裸名)时, + // 成员既无注解也无赋值,但仍是枚举成员(取值由编译期推导)。 + isMemberOfEnumeration = true; } // The spec specifically excludes names that start and end with a single underscore. diff --git a/packages/vipeye-internal/src/analyzer/typeEvaluator.ts b/packages/vipeye-internal/src/analyzer/typeEvaluator.ts index 97aa70a7..c26c830f 100644 --- a/packages/vipeye-internal/src/analyzer/typeEvaluator.ts +++ b/packages/vipeye-internal/src/analyzer/typeEvaluator.ts @@ -19401,6 +19401,14 @@ export function createTypeEvaluator( } } + // Viper 扩展:`enum X:` 语法声明的枚举隐式继承 t.CEnum。 + // 标记 EnumClass 使枚举成员访问(如 ASTFlag.IsAsync)返回枚举类型 + // 而不是成员的声明类型(与 class X(t.CEnum) 的语义一致); + // 标记 ViperEnumClass 使枚举可隐式转换为 int(等价 CEnum 子类)。 + if (node.d.isViperEnum) { + classType.shared.flags |= ClassTypeFlags.EnumClass | ClassTypeFlags.ViperEnumClass; + } + // Make sure we don't have 'object' derive from itself. Infinite // recursion will result. if ( @@ -26842,9 +26850,11 @@ export function createTypeEvaluator( // C 语言中 enum 值可以隐式转换为 int(如 int x = FR_OK)。 // Viper 的 CEnum 子类(如 FRESULT)的成员(如 FRESULT.FR_OK) // 可以赋值给 int 类型的变量/字段。 + // `enum X:` 语法声明的枚举(ViperEnumClass)等价 CEnum 子类,同样适用。 if ( isClassInstance(destType) && ClassType.isBuiltIn(destType, 'int') && - isClassInstance(srcType) && isCEnumSubclassType(srcType) + isClassInstance(srcType) && + (isCEnumSubclassType(srcType) || ClassType.isViperEnumClass(srcType)) ) { return true; } diff --git a/packages/vipeye-internal/src/analyzer/types.ts b/packages/vipeye-internal/src/analyzer/types.ts index a34a9736..44b4b157 100644 --- a/packages/vipeye-internal/src/analyzer/types.ts +++ b/packages/vipeye-internal/src/analyzer/types.ts @@ -670,6 +670,11 @@ export const enum ClassTypeFlags { // This class is rejected when used as the second argument to // an isinstance or issubclass call. IllegalIsinstanceClass = 1 << 24, + + // Viper 扩展:由 `enum X:` 语法声明的枚举类(等价 `class X(t.CEnum):`)。 + // 与 EnumClass 同时设置。用于把 Viper 枚举视作 CEnum 子类 + // (如 enum → int 的隐式转换)。 + ViperEnumClass = 1 << 25, } export interface DataClassBehaviors { @@ -1255,6 +1260,11 @@ export namespace ClassType { return !!(classType.shared.flags & ClassTypeFlags.EnumClass); } + // Viper 扩展:是否为 `enum X:` 语法声明的枚举类。 + export function isViperEnumClass(classType: ClassType) { + return !!(classType.shared.flags & ClassTypeFlags.ViperEnumClass); + } + export function isPropertyClass(classType: ClassType) { return !!(classType.shared.flags & ClassTypeFlags.PropertyClass); } diff --git a/packages/vipeye-internal/src/languageService/hoverProvider.ts b/packages/vipeye-internal/src/languageService/hoverProvider.ts index c760cbcb..c2468a63 100644 --- a/packages/vipeye-internal/src/languageService/hoverProvider.ts +++ b/packages/vipeye-internal/src/languageService/hoverProvider.ts @@ -19,6 +19,7 @@ import { isUnresolvedAliasDeclaration, } from '../analyzer/declaration'; import { getFileInfo } from '../analyzer/analyzerNodeInfo'; +import { getViperEnumMemberValues } from '../analyzer/enums'; import * as ParseTreeUtils from '../analyzer/parseTreeUtils'; import { SourceMapper } from '../analyzer/sourceMapper'; import { SynthesizedTypeInfo } from '../analyzer/symbol'; @@ -221,6 +222,24 @@ export function addDocumentationResultsPart( fail(`Unsupported markup type: ${format}`); } +// Viper 扩展:构造 `enum X:` 枚举成员的 hover 类型文本(枚举类型 + 编译期推导值), +// 例如 `ASTFlag = 0`。非 `enum X:` 的成员返回 undefined,走常规显示逻辑。 +function getViperEnumMemberTypeText(declaration: VariableDeclaration | undefined, name: string): string | undefined { + if (!declaration || declaration.node.nodeType !== ParseNodeType.Name) { + return undefined; + } + + const classNode = ParseTreeUtils.getEnclosingClass(declaration.node, /* stopAtFunction */ true); + if (!classNode?.d.isViperEnum) { + return undefined; + } + + const className = classNode.d.name.d.value; + const value = getViperEnumMemberValues(classNode).get(name); + + return value === undefined ? `${name}: ${className}` : `${name}: ${className} = ${value}`; +} + export function getVariableTypeText( evaluator: TypeEvaluator, declaration: VariableDeclaration | undefined, @@ -236,6 +255,13 @@ export function getVariableTypeText( label = declaration.isConstant || evaluator.isFinalVariableDeclaration(declaration) ? 'constant' : 'variable'; } + // Viper 扩展:`enum X:` 枚举成员显示枚举类型与编译期推导值(如 `ASTFlag = 0`)。 + // 裸名成员没有赋值也没有注解,常规路径只会给出 Unbound。 + const viperEnumMemberText = getViperEnumMemberTypeText(declaration, name); + if (viperEnumMemberText) { + return `(${label}) ` + viperEnumMemberText; + } + let typeVarName: string | undefined; if (type.props?.typeAliasInfo && typeNode.nodeType === ParseNodeType.Name) { diff --git a/packages/vipeye-internal/src/localization/localize.ts b/packages/vipeye-internal/src/localization/localize.ts index 46ecc236..f1233f2b 100644 --- a/packages/vipeye-internal/src/localization/localize.ts +++ b/packages/vipeye-internal/src/localization/localize.ts @@ -463,6 +463,14 @@ export namespace Localizer { export const enumMemberSet = () => new ParameterizedString<{ name: string }>(getRawString('Diagnostic.enumMemberSet')); export const enumMemberTypeAnnotation = () => getRawString('Diagnostic.enumMemberTypeAnnotation'); + export const viperEnumValueOutOfRange = () => + new ParameterizedString<{ value: string; type: string; min: string; max: string }>( + getRawString('Diagnostic.viperEnumValueOutOfRange') + ); + export const viperIntegerLiteralOutOfRange = () => + new ParameterizedString<{ value: string; type: string; min: string; max: string }>( + getRawString('Diagnostic.viperIntegerLiteralOutOfRange') + ); export const viperBeforeInitReserved = () => getRawString('Diagnostic.viperBeforeInitReserved'); export const exceptionGroupIncompatible = () => getRawString('Diagnostic.exceptionGroupIncompatible'); export const exceptGroupMismatch = () => getRawString('Diagnostic.exceptGroupMismatch'); 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 9a3896ac..632d6e6f 100644 --- a/packages/vipeye-internal/src/localization/package.nls.en-us.json +++ b/packages/vipeye-internal/src/localization/package.nls.en-us.json @@ -354,6 +354,14 @@ "message": "Type annotations are not allowed for enum members", "comment": "{Locked='enum'}" }, + "viperEnumValueOutOfRange": { + "message": "Enum member value {value} is out of range for type \"{type}\" (representable range is {min} to {max})", + "comment": "{Locked='{value}'}{Locked='{type}'}{Locked='{min}'}{Locked='{max}'}" + }, + "viperIntegerLiteralOutOfRange": { + "message": "Type \"{type}\" cannot correctly store the number {value} (representable range is {min} to {max}); widen the type", + "comment": "{Locked='{value}'}{Locked='{type}'}{Locked='{min}'}{Locked='{max}'}" + }, "viperBeforeInitReserved": { "message": "\"__before_init__\" is auto-generated by the compiler and cannot be defined manually", "comment": "{Locked='__before_init__'}" 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 04259a76..4d138739 100644 --- a/packages/vipeye-internal/src/localization/package.nls.zh-cn.json +++ b/packages/vipeye-internal/src/localization/package.nls.zh-cn.json @@ -161,6 +161,8 @@ "enumMemberDelete": "无法删除 Enum 成员 \"{name}\"", "enumMemberSet": "无法分配 Enum 成员 \"{name}\"", "enumMemberTypeAnnotation": "enum 成员不允许使用类型批注", + "viperEnumValueOutOfRange": "枚举成员的值 {value} 超出类型 \"{type}\" 的可表示范围({min} ~ {max})", + "viperIntegerLiteralOutOfRange": "类型 \"{type}\" 无法正确存储数字 {value}(可表示范围 {min} ~ {max}),需要扩容", "viperBeforeInitReserved": "\"__before_init__\" 由编译器自动生成,不允许手动定义", "exceptGroupMismatch": "Try 语句不能同时包含 \"except\" 和 \"except*\"", "exceptGroupRequiresType": "异常组语法(\"except*\")需要异常类型", diff --git a/packages/vipeye-internal/src/parser/parseNodes.ts b/packages/vipeye-internal/src/parser/parseNodes.ts index d3c76cc9..78a14191 100644 --- a/packages/vipeye-internal/src/parser/parseNodes.ts +++ b/packages/vipeye-internal/src/parser/parseNodes.ts @@ -568,6 +568,10 @@ export interface ClassNode extends ParseNodeBase { typeParams: TypeParameterListNode | undefined; arguments: ArgumentNode[]; suite: SuiteNode; + + // Viper 扩展:由 `enum X:` 语法声明(等价 `class X(t.CEnum):`)。 + // 成员可以只写成员名(裸名 / 逗号分隔的裸名),值由编译期推导。 + isViperEnum: boolean; }; } @@ -587,6 +591,7 @@ export namespace ClassNode { typeParams, arguments: [], suite: suite, + isViperEnum: false, }, }; @@ -650,6 +655,7 @@ export namespace ClassNode { a: undefined, d: { statements: [], typeComment: undefined }, }, + isViperEnum: false, }, }; diff --git a/packages/vipeye-internal/src/parser/parser.ts b/packages/vipeye-internal/src/parser/parser.ts index 6fbe1eb6..912884b8 100644 --- a/packages/vipeye-internal/src/parser/parser.ts +++ b/packages/vipeye-internal/src/parser/parser.ts @@ -489,6 +489,14 @@ export class Parser { return this._parseFunctionDef(); case KeywordType.Define: + // Viper 扩展:`define NAME: 类型 = 值` / `define NAME = 值` 为编译期常量声明 + // (值必须能存入声明的类型位宽,见 checker 的整数字面量位宽校验)。 + // 与 `define f(...)`(宏函数,等价 def)区分:常量声明的名字后跟 ':' 或 '='。 + if (this._isViperDefineConstDeclaration()) { + this._getNextToken(); + return this._parseSimpleStatement(); + } + // Viper 扩展:define 宏函数(与 def 语法一致,语义为编译期宏展开)。 return this._parseFunctionDef(); @@ -1590,7 +1598,14 @@ export class Parser { } // suite: ':' (simple_stmt | NEWLINE INDENT stmt+ DEDENT) - private _parseSuite(isFunction = false, skipBody = false, postColonCallback?: () => void): SuiteNode { + // allowTrailingIndent(Viper 扩展):允许 simple_stmt 之后再接 NEWLINE INDENT stmt+ DEDENT, + // 用于 `enum X: a, b, c` 后换行 + TAB 续写枚举成员。 + private _parseSuite( + isFunction = false, + skipBody = false, + postColonCallback?: () => void, + allowTrailingIndent = false + ): SuiteNode { const nextToken = this._peekToken(); const suite = SuiteNode.create(nextToken); @@ -1734,6 +1749,34 @@ export class Parser { const simpleStatement = this._parseSimpleStatement(); suite.d.statements.push(simpleStatement); simpleStatement.parent = suite; + + // Viper 扩展:`enum X: a, b, c` 允许换行 + TAB 继续写成员(等效缩进块)。 + // 注:_parseSimpleStatement 已吃掉行尾的 NewLine,这里只需看 Indent。 + if (allowTrailingIndent && this._peekTokenType() === TokenType.Indent) { + const bodyIndentToken = this._getNextToken() as IndentToken; + if (bodyIndentToken.isIndentAmbiguous) { + this._addSyntaxError(LocMessage.inconsistentTabs(), bodyIndentToken); + } + + while (true) { + const nextToken = this._peekToken(); + if (nextToken.type === TokenType.Dedent || nextToken.type === TokenType.EndOfStream) { + if (nextToken.type === TokenType.Dedent) { + this._getNextToken(); + } + break; + } + + const statement = this._parseStatement(); + if (!statement) { + // Perform basic error recovery to get to the next line. + this._consumeTokensUntilType([TokenType.NewLine]); + } else { + statement.parent = suite; + suite.d.statements.push(statement); + } + } + } } if (suite.d.statements.length > 0) { @@ -2620,6 +2663,21 @@ export class Parser { return false; } + // Viper 扩展:判断 `define` 之后是否为编译期常量声明(`NAME: 类型 = 值` / `NAME = 值`)。 + // 位置基于 define 关键字:`_peekToken(1)` 是名字,`_peekToken(2)` 是 ':' 或 '='。 + private _isViperDefineConstDeclaration(): boolean { + if (this._peekToken(1).type !== TokenType.Identifier) { + return false; + } + + const afterName = this._peekToken(2); + if (afterName.type === TokenType.Colon) { + return true; + } + + return afterName.type === TokenType.Operator && (afterName as OperatorToken).operatorType === OperatorType.Assign; + } + // classdef: 'class' NAME ['(' [arglist] ')'] suite private _parseClassDef(decorators?: DecoratorNode[]): ClassNode { const classToken = this._getKeywordToken(KeywordType.Class); @@ -2653,7 +2711,12 @@ export class Parser { } } - const suite = this._parseSuite(/* isFunction */ false, this._parseOptions.skipFunctionAndClassBody); + const suite = this._parseSuite( + /* isFunction */ false, + this._parseOptions.skipFunctionAndClassBody, + /* postColonCallback */ undefined, + /* allowTrailingIndent */ classToken.isViperEnum === true + ); const classNode = ClassNode.create(classToken, NameNode.create(nameToken), suite, typeParameters); classNode.d.arguments = argList; @@ -2661,6 +2724,11 @@ export class Parser { arg.parent = classNode; }); + // Viper 扩展:`enum X:` 语法声明的枚举(tokenizer 在 class 关键字 token 上打标记)。 + if (classToken.isViperEnum) { + classNode.d.isViperEnum = true; + } + if (decorators) { classNode.d.decorators = decorators; if (decorators.length > 0) { diff --git a/packages/vipeye-internal/src/parser/tokenizer.ts b/packages/vipeye-internal/src/parser/tokenizer.ts index f43f5b7f..6bb8ca60 100644 --- a/packages/vipeye-internal/src/parser/tokenizer.ts +++ b/packages/vipeye-internal/src/parser/tokenizer.ts @@ -647,11 +647,14 @@ enum MagicsKind { // Viper 支持将 C 修饰/声明语义写成语句前缀关键字: // inline/export/extern def f(...) —— 修饰关键字,必须与 def 搭配 // fn/fun/func/function f(...) —— def 的别名 -// enum/rnum class|struct Foo —— 等价 t.CEnum / t.REnum -// enum Foo: / rnum Foo: —— 单独使用(等价 class Foo:) +// enum/rnum class|struct Foo —— 跳过前缀关键字(后续 class 正常解析) +// enum Foo: —— 等价 `class Foo(t.CEnum):`(枚举) +// rnum Foo: / union Foo: —— 等价 class Foo: // struct Foo: —— 独立的 struct 关键字(等价 class) // 返回 'skip':跳过该关键字 token(后续 def/class 正常解析,位置与缩进不变); -// 返回 'class':把 enum/rnum/struct 当作 class 关键字; +// 返回 'class':把 rnum/union/struct 当作 class 关键字; +// 返回 'classEnum':把 enum 当作 class 关键字,并标记为 Viper 枚举 +// (等价 `class Foo(t.CEnum)`,analyzer 会赋予 CEnum 枚举语义); // 返回 'def':把 fn/fun/func/function 当作 def 关键字; // 返回 'viperModifier':inline/export/extern 生成修饰关键字 token,由 parser // 判断是否与 def 搭配(搭配则跳过并记录,单独使用则报错); @@ -662,7 +665,7 @@ function getViperStatementKeywordAction( identifierEnd: number, name: string, precededByViperModifier: boolean -): 'skip' | 'class' | 'def' | 'viperModifier' | undefined { +): 'skip' | 'class' | 'classEnum' | 'def' | 'viperModifier' | undefined { const nextWord = peekViperKeywordWord(text, identifierEnd); if (name === 'inline' || name === 'export' || name === 'extern') { // 修饰关键字仅在语句起始处生效,避免误伤 `self.inline` 等成员名。 @@ -694,12 +697,16 @@ function getViperStatementKeywordAction( if (nextWord === 'class' || nextWord === 'struct') { return 'skip'; } - // `enum A:` / `rnum A:` / `union A:` 单独使用(等价 class A:)。 - return isViperAtStatementStart(text, identifierStart) && - nextWord.length > 0 && - isIdentifierStartChar(nextWord.charCodeAt(0)) - ? 'class' - : undefined; + // `enum A:` / `rnum A:` / `union A:` 单独使用。 + if ( + !isViperAtStatementStart(text, identifierStart) || + nextWord.length === 0 || + !isIdentifierStartChar(nextWord.charCodeAt(0)) + ) { + return undefined; + } + // `enum A:` 等价 `class A(t.CEnum):`;`rnum A:` / `union A:` 等价 `class A:`。 + return name === 'enum' ? 'classEnum' : 'class'; } if (name === 'struct') { // 仅当 struct 位于语句起始(行首仅空白)且后跟标识符(类名)时才视为 class, @@ -1519,6 +1526,13 @@ export class Tokenizer { this._tokens.push(KeywordToken.create(start, length, KeywordType.Class, this._getComments())); return true; } + if (viperAction === 'classEnum') { + // `enum Foo:` → `class Foo(t.CEnum):`:合成带枚举标记的 class 关键字 token。 + this._tokens.push( + KeywordToken.create(start, length, KeywordType.Class, this._getComments(), /* isViperEnum */ true) + ); + return true; + } if (viperAction === 'def') { this._tokens.push(KeywordToken.create(start, length, KeywordType.Def, this._getComments())); return true; diff --git a/packages/vipeye-internal/src/parser/tokenizerTypes.ts b/packages/vipeye-internal/src/parser/tokenizerTypes.ts index 608d1cf0..addd3807 100644 --- a/packages/vipeye-internal/src/parser/tokenizerTypes.ts +++ b/packages/vipeye-internal/src/parser/tokenizerTypes.ts @@ -343,6 +343,11 @@ export namespace NewLineToken { export interface KeywordToken extends Token { readonly type: TokenType.Keyword; readonly keywordType: KeywordType; + + // Viper 扩展:该 class 关键字由 Viper 的 `enum X:` 语法合成 + // (等价于 `class X(t.CEnum):`)。parser 据此在 ClassNode 上打标记, + // analyzer 据此赋予枚举语义。 + readonly isViperEnum?: boolean; } export namespace KeywordToken { @@ -350,7 +355,8 @@ export namespace KeywordToken { start: number, length: number, keywordType: KeywordType, - comments: Comment[] | undefined + comments: Comment[] | undefined, + isViperEnum = false ): KeywordToken { if (comments !== undefined) { return { @@ -359,6 +365,7 @@ export namespace KeywordToken { type: TokenType.Keyword, comments, keywordType, + isViperEnum, }; } @@ -367,6 +374,7 @@ export namespace KeywordToken { length, type: TokenType.Keyword, keywordType, + isViperEnum, }; } diff --git a/packages/vipeye-internal/src/tests/__define_const__/define_const_0.vp b/packages/vipeye-internal/src/tests/__define_const__/define_const_0.vp new file mode 100644 index 00000000..c5580bfc --- /dev/null +++ b/packages/vipeye-internal/src/tests/__define_const__/define_const_0.vp @@ -0,0 +1,12 @@ +import t + +define MAX_SIZE: int = 1024 +define PAGE_SIZE = 4096 +define PAGE_BYTES: t.CUInt64T = 18446744073709551615 +define SMALL: t.CInt8T = -128 +define TINY: t.i8 = 127 + +def use() -> int: + a: int = MAX_SIZE + b: int = PAGE_SIZE + return a + b diff --git a/packages/vipeye-internal/src/tests/__define_const__/define_const_1.vp b/packages/vipeye-internal/src/tests/__define_const__/define_const_1.vp new file mode 100644 index 00000000..a0c5a8c8 --- /dev/null +++ b/packages/vipeye-internal/src/tests/__define_const__/define_const_1.vp @@ -0,0 +1,5 @@ +define square(x: int) -> int: + return x * x + +def use() -> int: + return square(3) diff --git a/packages/vipeye-internal/src/tests/__define_const__/define_const_2.vp b/packages/vipeye-internal/src/tests/__define_const__/define_const_2.vp new file mode 100644 index 00000000..163167d8 --- /dev/null +++ b/packages/vipeye-internal/src/tests/__define_const__/define_const_2.vp @@ -0,0 +1,11 @@ +import t + +define BIG_INT: int = 10000000000000000000000000000000 +PLAIN_BIG: int = 10000000000000000000000000000000 +define SMALL_ENUM: t.CInt8T = 200 +define OK_BIG: t.CUInt64T = 18446744073709551615 +define OK_NEG: int = -2147483648 +define OK_POS: int = 2147483647 + +def use() -> int: + return 0 diff --git a/packages/vipeye-internal/src/tests/__define_const__/define_const_3.vp b/packages/vipeye-internal/src/tests/__define_const__/define_const_3.vp new file mode 100644 index 00000000..26a7a88a --- /dev/null +++ b/packages/vipeye-internal/src/tests/__define_const__/define_const_3.vp @@ -0,0 +1,9 @@ +import t + +define MAX_SIZE: int = 1024 +define FLAG = 1 + +def use() -> int: + a: int = MAX_SIZE + b: int = FLAG + return a + b diff --git a/packages/vipeye-internal/src/tests/__enum_flag__/enum_flag_0.vp b/packages/vipeye-internal/src/tests/__enum_flag__/enum_flag_0.vp new file mode 100644 index 00000000..7d07126a --- /dev/null +++ b/packages/vipeye-internal/src/tests/__enum_flag__/enum_flag_0.vp @@ -0,0 +1,19 @@ +import t + +class CEnumRef(t.CEnum): + RED: t.State = 0 + GREEN = 1 + +enum ASTFlag: + IsAsync + Simple + HasStar: int + +enum Mid: + a + b = 5 + c: t.u8 + +def use() -> int: + v: int = ASTFlag.IsAsync + return 0 diff --git a/packages/vipeye-internal/src/tests/__enum_flag__/enum_flag_1.vp b/packages/vipeye-internal/src/tests/__enum_flag__/enum_flag_1.vp new file mode 100644 index 00000000..a8fc8787 --- /dev/null +++ b/packages/vipeye-internal/src/tests/__enum_flag__/enum_flag_1.vp @@ -0,0 +1,12 @@ +import t + +enum ASTFlag: + IsAsync + Simple + HasStar: int + +def use_enum() -> int: + a: int = ASTFlag.IsAsync + b: int = ASTFlag.Simple + c: int = ASTFlag.HasStar + return 0 diff --git a/packages/vipeye-internal/src/tests/__enum_flag__/enum_flag_2.vp b/packages/vipeye-internal/src/tests/__enum_flag__/enum_flag_2.vp new file mode 100644 index 00000000..d0ac58c9 --- /dev/null +++ b/packages/vipeye-internal/src/tests/__enum_flag__/enum_flag_2.vp @@ -0,0 +1,8 @@ +import t + +enum X: a, b, c + d + +def use_x() -> int: + v: int = X.d + return 0 diff --git a/packages/vipeye-internal/src/tests/__enum_flag__/enum_flag_3.vp b/packages/vipeye-internal/src/tests/__enum_flag__/enum_flag_3.vp new file mode 100644 index 00000000..4e1e94a7 --- /dev/null +++ b/packages/vipeye-internal/src/tests/__enum_flag__/enum_flag_3.vp @@ -0,0 +1,10 @@ +import t + +enum Small: + a + b + c: t.i1 + +def use_small() -> int: + v: int = Small.a + return 0 diff --git a/packages/vipeye-internal/src/tests/__enum_flag__/enum_flag_4.vp b/packages/vipeye-internal/src/tests/__enum_flag__/enum_flag_4.vp new file mode 100644 index 00000000..3922be9b --- /dev/null +++ b/packages/vipeye-internal/src/tests/__enum_flag__/enum_flag_4.vp @@ -0,0 +1,10 @@ +import t + +enum Mid: + a + b = 5 + c: t.u8 + +def use_mid() -> int: + v: int = Mid.c + return 0 diff --git a/packages/vipeye-internal/src/tests/viperDefineConst.test.ts b/packages/vipeye-internal/src/tests/viperDefineConst.test.ts new file mode 100644 index 00000000..3bc36cbb --- /dev/null +++ b/packages/vipeye-internal/src/tests/viperDefineConst.test.ts @@ -0,0 +1,243 @@ +/* + * viperDefineConst.test.ts + * + * 验证 Viper 的 `define` 编译期常量声明与整数字面量位宽校验: + * - `define NAME: 类型 = 值` / `define NAME = 值`(后者由编译器推导类型大小); + * - `define f(...)`(宏函数,等价 def)不受影响; + * - 字面量无法存入声明类型位宽时报错(如 int 为 i32 存不下 10^31), + * 非 define 的普通声明形式同样报错(类型大小无法正确存储数字,需要扩容)。 + * + * 运行:npx jest src/tests/viperDefineConst.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 { FullAccessHost } from '../common/fullAccessHost'; +import { convertOffsetToPosition } from '../common/positionUtils'; +import { RealTempFile, createFromRealFileSystem } from '../common/realFileSystem'; +import { createServiceProvider } from '../common/serviceProviderExtensions'; +import { UriEx } from '../common/uri/uriUtils'; +import { FunctionNode, MemberAccessNode } from '../parser/parseNodes'; +import { KeywordType } from '../parser/tokenizerTypes'; + +(global as any).__rootDirectory = path.resolve(); + +const TRANSPYC_INCLUDES = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes'; + +let testFileIndex = 0; + +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, '__define_const__'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, `define_const_${testFileIndex++}.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}`; + }); +} + +test('define NAME: 类型 = 值 / define NAME = 值 解析为普通声明,值大小可存入类型', () => { + const code = `import t + +define MAX_SIZE: int = 1024 +define PAGE_SIZE = 4096 +define PAGE_BYTES: t.CUInt64T = 18446744073709551615 +define SMALL: t.CInt8T = -128 +define TINY: t.i8 = 127 + +def use() -> int: + a: int = MAX_SIZE + b: int = PAGE_SIZE + return a + b +`; + const { program, configOptions, parseResults, uri, serviceProvider } = buildProgramForCode(code); + + // define 常量声明被解析为普通赋值语句(不是函数定义)。 + const functions: number[] = []; + const evaluator = program.evaluator as TypeEvaluator | undefined; + assert(evaluator, 'evaluator not available'); + const lines = parseResults.parserOutput.lines; + class Collector extends ParseTreeWalker { + override visitFunction(node: FunctionNode) { + functions.push(convertOffsetToPosition(node.start, lines).line + 1); + return true; + } + } + new Collector().walk(parseResults.parserOutput.parseTree); + assert.deepStrictEqual(functions, [9], `define 常量声明不应被当作函数定义:${JSON.stringify(functions)}`); + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== 诊断 ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + const bad = diagnostics.filter((d) => /未定义|undefined|not defined|未使用|no effect|unused|无法正确存储/i.test(d)); + assert.strictEqual(bad.length, 0, `不应报未定义/未使用/位宽错误:${JSON.stringify(diagnostics, undefined, 2)}`); + + program.dispose(); + serviceProvider.dispose(); +}); + +test('define 宏函数(define f(...))仍解析为函数定义', () => { + const code = `define square(x: int) -> int: + return x * x + +def use() -> int: + return square(3) +`; + const { program, configOptions, parseResults, uri, serviceProvider } = buildProgramForCode(code); + + const lines = parseResults.parserOutput.lines; + const functions: { line: number; isDefineKeyword: boolean }[] = []; + class Collector extends ParseTreeWalker { + override visitFunction(node: FunctionNode) { + functions.push({ + line: convertOffsetToPosition(node.start, lines).line + 1, + isDefineKeyword: (node.d.firstToken as any).keywordType === KeywordType.Define, + }); + return true; + } + } + new Collector().walk(parseResults.parserOutput.parseTree); + // L1 的 define 宏函数 + L4 的普通 def 都应解析为函数;只有 define 那个带 isDefine 标记。 + assert.strictEqual(functions.length, 2, `应解析出 2 个函数:${JSON.stringify(functions)}`); + assert(functions[0].isDefineKeyword, `define 宏函数应保留 define 关键字标记:${JSON.stringify(functions)}`); + assert(!functions[1].isDefineKeyword, `普通 def 不应带 define 标记:${JSON.stringify(functions)}`); + + const diagnostics = getDiagnostics(program, configOptions, uri); + const errors = diagnostics.filter((d) => d.includes('category=0') || d.includes('category=1')); + assert.strictEqual(errors.length, 0, `不应有错误:${JSON.stringify(diagnostics, undefined, 2)}`); + + program.dispose(); + serviceProvider.dispose(); +}); + +test('字面量超出声明类型位宽时报错(define 与普通声明都报)', () => { + const code = `import t + +define BIG_INT: int = 10000000000000000000000000000000 +PLAIN_BIG: int = 10000000000000000000000000000000 +define SMALL_ENUM: t.CInt8T = 200 +define OK_BIG: t.CUInt64T = 18446744073709551615 +define OK_NEG: int = -2147483648 +define OK_POS: int = 2147483647 + +def use() -> int: + return 0 +`; + const { program, configOptions, uri, serviceProvider } = buildProgramForCode(code); + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== 诊断 ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + const rangeErrors = diagnostics.filter((d) => d.includes('需要扩容')); + const errorLines = rangeErrors.map((d) => d.match(/\[line=(\d+)\]/)?.[1]); + assert.deepStrictEqual( + errorLines.sort(), + ['3', '4', '5'], + `应只在 L3(define 超长)/L4(普通声明超长)/L5(CInt8T 存不下 200)报错:${JSON.stringify( + diagnostics, + undefined, + 2 + )}` + ); + assert(rangeErrors.every((d) => d.includes('10000000000000000000000000000000') || d.includes('200')), JSON.stringify(rangeErrors)); + + program.dispose(); + serviceProvider.dispose(); +}); + +test('define 常量与普通常量在引用处类型正确', () => { + const code = `import t + +define MAX_SIZE: int = 1024 +define FLAG = 1 + +def use() -> int: + a: int = MAX_SIZE + b: int = FLAG + return a + b +`; + const { program, configOptions, parseResults, uri, serviceProvider } = buildProgramForCode(code); + + const evaluator = program.evaluator as TypeEvaluator | undefined; + assert(evaluator, 'evaluator not available'); + const lines = parseResults.parserOutput.lines; + const types: { line: number; type: string }[] = []; + class Collector extends ParseTreeWalker { + override visitMemberAccess(node: MemberAccessNode) { + return true; + } + override visitName(node: any) { + if (node.d.value === 'MAX_SIZE' || node.d.value === 'FLAG') { + const type = evaluator!.getType(node); + types.push({ + line: convertOffsetToPosition(node.start, lines).line + 1, + type: type ? program.printType(type) : '(undefined)', + }); + } + return true; + } + } + new Collector().walk(parseResults.parserOutput.parseTree); + console.log('=== 常量引用类型 ==='); + types.forEach((t) => console.log(` L${t.line}: ${t.type}`)); + + const usageTypes = types.filter((t) => t.line >= 7); + assert.strictEqual(usageTypes.length, 2, `应收集到 2 处引用:${JSON.stringify(types)}`); + usageTypes.forEach((t) => { + assert(!t.type.includes('Unknown') && !t.type.includes('Unbound'), `引用类型不应退化:${JSON.stringify(t)}`); + }); + + program.dispose(); + serviceProvider.dispose(); +}); diff --git a/packages/vipeye-internal/src/tests/viperEnumFlag.test.ts b/packages/vipeye-internal/src/tests/viperEnumFlag.test.ts new file mode 100644 index 00000000..98c10127 --- /dev/null +++ b/packages/vipeye-internal/src/tests/viperEnumFlag.test.ts @@ -0,0 +1,311 @@ +/* + * viperEnumFlag.test.ts + * + * 验证 Viper 的 `enum X:` 语法(等价 `class X(t.CEnum):`): + * - 成员可以只写成员名(裸名 / 逗号分隔的裸名),不报"未定义变量/未使用表达式"; + * - 取值由编译期从 0 递增推导,中间可显式赋值(显式赋值后从该值 + 1 继续); + * - 推导值或显式赋值超出成员声明类型(如 i1)的可表示范围时报错; + * - `enum X: a, b, c` 之后可换行 + TAB 继续写成员。 + * + * 运行:npx jest src/tests/viperEnumFlag.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 { ParseTreeWalker } from '../analyzer/parseTreeWalker'; +import { TypeEvaluator } from '../analyzer/typeEvaluatorTypes'; +import { ConfigOptions, ExecutionEnvironment } from '../common/configOptions'; +import { NullConsole } from '../common/console'; +import { FullAccessHost } from '../common/fullAccessHost'; +import { convertOffsetToPosition } from '../common/positionUtils'; +import { RealTempFile, createFromRealFileSystem } from '../common/realFileSystem'; +import { createServiceProvider } from '../common/serviceProviderExtensions'; +import { UriEx } from '../common/uri/uriUtils'; +import { MemberAccessNode } from '../parser/parseNodes'; +import { HoverProvider } from '../languageService/hoverProvider'; + +(global as any).__rootDirectory = path.resolve(); + +const TRANSPYC_INCLUDES = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes'; + +let testFileIndex = 0; + +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, '__enum_flag__'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, `enum_flag_${testFileIndex++}.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}`; + }); +} + +// 收集文件中所有成员访问表达式(形如 `ASTFlag.IsAsync`)的类型。 +function collectMemberAccessTypes(program: Program, parseResults: any, memberNames: string[]) { + const evaluator = program.evaluator as TypeEvaluator | undefined; + assert(evaluator, 'evaluator not available'); + const ev = evaluator; + const lines = parseResults.parserOutput.lines; + const results: { line: number; member: string; type: string }[] = []; + + class Collector extends ParseTreeWalker { + override visitMemberAccess(node: MemberAccessNode) { + const memberName = (node.d.member as any)?.d?.value ?? ''; + if (memberNames.includes(memberName)) { + const pos = convertOffsetToPosition(node.start, lines); + const type = ev.getType(node as any); + results.push({ + line: pos.line + 1, + member: memberName, + type: type ? program.printType(type) : '(undefined)', + }); + } + return true; + } + } + new Collector().walk(parseResults.parserOutput.parseTree); + return results; +} + +function assertNoUndefinedOrUnused(diagnostics: string[]) { + const bad = diagnostics.filter( + (d) => /未定义|undefined|not defined|未使用|no effect|unused/i.test(d) + ); + assert.strictEqual(bad.length, 0, `不应报未定义/未使用:${JSON.stringify(diagnostics, undefined, 2)}`); +} + +// 取指定位置的 hover 文本(Markdown)。 +function getHoverText(program: Program, uri: any, line: number, character: number): string { + const provider = new HoverProvider( + program as any, + uri, + { line, character } as Position, + MarkupKind.Markdown, + CancellationToken.None + ); + const hover = provider.getHover(); + return hover ? (hover.contents as MarkupContent).value : '(无 hover)'; +} + +test('枚举成员 hover 显示枚举类型与推导值', () => { + const code = `import t + +class CEnumRef(t.CEnum): + RED: t.State = 0 + GREEN = 1 + +enum ASTFlag: + IsAsync + Simple + HasStar: int + +enum Mid: + a + b = 5 + c: t.u8 + +def use() -> int: + v: int = ASTFlag.IsAsync + return 0 +`; + const { program, uri, serviceProvider } = buildProgramForCode(code); + + const hovers: Record = { + '旧写法 RED: t.State = 0': getHoverText(program, uri, 3, 4), + '旧写法 GREEN = 1': getHoverText(program, uri, 4, 4), + '新写法裸名 IsAsync': getHoverText(program, uri, 7, 4), + '新写法 Simple': getHoverText(program, uri, 8, 4), + '新写法 HasStar: int': getHoverText(program, uri, 9, 4), + '显式赋值 b = 5': getHoverText(program, uri, 13, 4), + '显式赋值 + 类型 c: t.u8': getHoverText(program, uri, 14, 4), + '使用点 ASTFlag.IsAsync': getHoverText(program, uri, 17, 21), + }; + Object.entries(hovers).forEach(([key, value]) => { + console.log(`=== ${key} ===`); + console.log(value); + }); + + // `enum X:` 成员:显示枚举类型 + 编译期推导值(裸名成员不再显示 Unbound)。 + assert(hovers['新写法裸名 IsAsync'].includes('IsAsync: ASTFlag = 0'), hovers['新写法裸名 IsAsync']); + assert(hovers['新写法 Simple'].includes('Simple: ASTFlag = 1'), hovers['新写法 Simple']); + assert(hovers['新写法 HasStar: int'].includes('HasStar: ASTFlag = 2'), hovers['新写法 HasStar: int']); + // 显式赋值后从该值 +1 继续(b = 5 ⇒ c = 6)。 + assert(hovers['显式赋值 b = 5'].includes('b: Mid = 5'), hovers['显式赋值 b = 5']); + assert(hovers['显式赋值 + 类型 c: t.u8'].includes('c: Mid = 6'), hovers['显式赋值 + 类型 c: t.u8']); + // 使用点同样显示推导值(同一份声明)。 + assert(hovers['使用点 ASTFlag.IsAsync'].includes('IsAsync: ASTFlag = 0'), hovers['使用点 ASTFlag.IsAsync']); + // 旧写法(CEnum 子类)保持原样,不受影响。 + assert(hovers['旧写法 RED: t.State = 0'].includes('RED: t.State'), hovers['旧写法 RED: t.State = 0']); + + program.dispose(); + serviceProvider.dispose(); +}); + +test('enum X: 裸名成员被识别为枚举成员,取值从 0 递增', () => { + const code = `import t + +enum ASTFlag: + IsAsync + Simple + HasStar: int + +def use_enum() -> int: + a: int = ASTFlag.IsAsync + b: int = ASTFlag.Simple + c: int = ASTFlag.HasStar + return 0 +`; + const { program, configOptions, parseResults, uri, serviceProvider } = buildProgramForCode(code); + + const memberTypes = collectMemberAccessTypes(program, parseResults, ['IsAsync', 'Simple', 'HasStar']); + console.log('=== 成员访问类型 ==='); + memberTypes.forEach((m) => console.log(` L${m.line} .${m.member}: ${m.type}`)); + assert.strictEqual(memberTypes.length, 3, `成员访问未全部收集:${JSON.stringify(memberTypes)}`); + memberTypes.forEach((m) => { + assert(m.type.includes('ASTFlag'), `成员 .${m.member} 应解析为 ASTFlag 枚举类型:${m.type}`); + }); + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== 诊断 ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + assertNoUndefinedOrUnused(diagnostics); + + const errors = diagnostics.filter((d) => d.includes('category=0') || d.includes('category=1')); + assert.strictEqual(errors.length, 0, `不应有错误:${JSON.stringify(diagnostics, undefined, 2)}`); + + program.dispose(); + serviceProvider.dispose(); +}); + +test('enum X: a, b, c 同行使 + 换行 TAB 续写', () => { + const code = `import t + +enum X: a, b, c + d + +def use_x() -> int: + v: int = X.d + return 0 +`; + const { program, configOptions, parseResults, uri, serviceProvider } = buildProgramForCode(code); + + const memberTypes = collectMemberAccessTypes(program, parseResults, ['a', 'b', 'c', 'd']); + console.log('=== 成员访问类型 ==='); + memberTypes.forEach((m) => console.log(` L${m.line} .${m.member}: ${m.type}`)); + assert.strictEqual(memberTypes.length, 1, `应收集到 1 个成员访问:${JSON.stringify(memberTypes)}`); + assert(memberTypes[0].type.includes('X'), `成员 .d 应解析为 X 枚举类型:${memberTypes[0].type}`); + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== 诊断 ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + assertNoUndefinedOrUnused(diagnostics); + + const errors = diagnostics.filter((d) => d.includes('category=0') || d.includes('category=1')); + assert.strictEqual(errors.length, 0, `不应有错误:${JSON.stringify(diagnostics, undefined, 2)}`); + + program.dispose(); + serviceProvider.dispose(); +}); + +test('enum X: 推导值超出成员声明类型(i1 存不下 2)报错', () => { + const code = `import t + +enum Small: + a + b + c: t.i1 + +def use_small() -> int: + v: int = Small.a + return 0 +`; + const { program, configOptions, uri, serviceProvider } = buildProgramForCode(code); + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== 诊断 ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + assertNoUndefinedOrUnused(diagnostics); + + const rangeErrors = diagnostics.filter((d) => d.includes('i1') && d.includes('2')); + assert.strictEqual( + rangeErrors.length, + 1, + `应恰好报 1 个 i1 溢出错误:${JSON.stringify(diagnostics, undefined, 2)}` + ); + assert(rangeErrors[0].includes('line=6'), `应报在 c 所在行(第 6 行):${rangeErrors[0]}`); + + program.dispose(); + serviceProvider.dispose(); +}); + +test('enum X: 中间显式赋值后继续递增,取值在范围内不报错', () => { + const code = `import t + +enum Mid: + a + b = 5 + c: t.u8 + +def use_mid() -> int: + v: int = Mid.c + return 0 +`; + const { program, configOptions, uri, serviceProvider } = buildProgramForCode(code); + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== 诊断 ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + assertNoUndefinedOrUnused(diagnostics); + + const errors = diagnostics.filter((d) => d.includes('category=0') || d.includes('category=1')); + assert.strictEqual(errors.length, 0, `不应有错误:${JSON.stringify(diagnostics, undefined, 2)}`); + + program.dispose(); + serviceProvider.dispose(); +});