feat(vipeye-internal): 放行 virtual class 方法槽位赋值(vtable)
ViperOS devfs.vp 的 `serial0_file.open = serial0_open` 原先误报 「无法为类"devfs_file"的属性"open"赋值 / 参数名称不匹配 / 额外参数 "flags"」。 根因:devfs_file 是 virtual class,open/close/read/write/ioctl 是虚方法 (vtable 槽)。该赋值写的是 vtable 槽;编译器只把 RHS 函数 bitcast i8* 后 store,不校验签名与 self。而 LSP 走普通成员赋值路径,比较的是 **绑定方法类型**(self 已剥掉),于是 RHS 显式写的 self: rawptr 成了多余参数。 修法:typeEvaluator 的 usage.method === 'set' 分支里,assignType 之前加 isViperVtableMethodSlotAssignment 命中即视为兼容、跳过报错与收窄。判据: - 类是 virtual class(ClassNode.d.viperClassModifier === 'virtual' 或 @t.CVTable 装饰器);注意传入的 classType 是**实例**形态,必须先转 可实例化形态(第一版没生效的原因) - 成员在类里声明为方法(FunctionType) - RHS 的 self 为泛指针(rawptr / t.CPtr / void*)或该类本身/父类的指针 - 其余非变参参数逐个 name + isTypeSame 相同,返回类型 isTypeSame 相同 (*args / **kwargs 不参与比较) 未放行:非 virtual class、self 非指针、参数名/类型/个数不符、返回类型不符。 - 新增 viperVtableMethodAssign.test.ts(3 用例) - .gitignore:新增 3 个 __viper_vtable_assign_* 产物目录 - CHANGELOG:1.1.426 追加 1 条
This commit is contained in:
1 parent
d91fc2d67e
commit
bc500d060f
4 files changed
+437
-22
No files matched your search
@@ -148,6 +148,9 @@ packages/vipeye-internal/src/tests/__viper_attribute_implicit_scope__/
|
||||
packages/vipeye-internal/src/tests/__viper_attribute_unknown__/
|
||||
packages/vipeye-internal/src/tests/__viper_ptr_unary__/
|
||||
packages/vipeye-internal/src/tests/__viper_ptr_convert__/
|
||||
packages/vipeye-internal/src/tests/__viper_vtable_assign_ok__/
|
||||
packages/vipeye-internal/src/tests/__viper_vtable_assign_bad__/
|
||||
packages/vipeye-internal/src/tests/__viper_vtable_assign_plain__/
|
||||
packages/vipeye-internal/src/tests/__viper_volatile__/
|
||||
packages/vipeye-internal/src/tests/__viper_volatile_bad__/
|
||||
packages/vipeye-internal/src/tests/__viper_callable__/
|
||||
|
||||
@@ -6894,6 +6894,133 @@ export function createTypeEvaluator(
|
||||
};
|
||||
}
|
||||
|
||||
// Viper 扩展:判断类是否为「virtual class」(启用多态 vtable)。
|
||||
// 两种等价写法:`virtual class X`(parser 记在 ClassNode.d.viperClassModifier)
|
||||
// 与 `@t.CVTable class X`(装饰器)。
|
||||
function isViperVirtualClass(classType: ClassType): boolean {
|
||||
const decl = classType.shared.declaration;
|
||||
const classNode = decl?.type === DeclarationType.Class ? decl.node : undefined;
|
||||
if (!classNode) {
|
||||
return false;
|
||||
}
|
||||
const fileInfo = AnalyzerNodeInfo.getFileInfo(classNode);
|
||||
if (!IsViperDialect(fileInfo.executionEnvironment.dialect)) {
|
||||
return false;
|
||||
}
|
||||
if (classNode.d.viperClassModifier === 'virtual') {
|
||||
return true;
|
||||
}
|
||||
return classNode.d.decorators.some((decorator) => {
|
||||
const expr = decorator.d.expr;
|
||||
const target = expr.nodeType === ParseNodeType.Call ? expr.d.leftExpr : expr;
|
||||
return (
|
||||
(target.nodeType === ParseNodeType.MemberAccess && target.d.member.d.value === 'CVTable') ||
|
||||
(target.nodeType === ParseNodeType.Name && target.d.value === 'CVTable')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Viper 扩展:vtable 槽位赋值中 RHS 的 `self` 类型是否可接受。
|
||||
// 允许:泛指针(rawptr / t.CPtr,即 pointee 未定(TypeVar/Unknown)或 void*),
|
||||
// 或「该类本身 / 其父类」的指针(如 `self: devfs_file*`)。
|
||||
function isViperVtableSelfTypeOk(selfType: Type, classType: ClassType): boolean {
|
||||
if (!isClassInstance(selfType) || !isCPtrType(selfType)) {
|
||||
return false;
|
||||
}
|
||||
const typeArgs = selfType.priv.typeArgs;
|
||||
if (!typeArgs || typeArgs.length === 0) {
|
||||
return true;
|
||||
}
|
||||
const pointee = typeArgs[0];
|
||||
if (isTypeVar(pointee) || isUnknown(pointee) || isCVoidType(pointee)) {
|
||||
return true;
|
||||
}
|
||||
if (!isClass(pointee)) {
|
||||
return false;
|
||||
}
|
||||
const pointeeInstantiable = isInstantiableClass(pointee) ? pointee : ClassType.cloneAsInstantiable(pointee);
|
||||
const inheritanceChain: InheritanceChain = [];
|
||||
return ClassType.isDerivedFrom(classType, pointeeInstantiable, inheritanceChain);
|
||||
}
|
||||
|
||||
// Viper 扩展:判断 `obj.method = func` 是否为 virtual class(vtable)的
|
||||
// **方法槽位赋值**,且按 vtable 语义应当放行。
|
||||
//
|
||||
// 背景:`serial0_file.open = serial0_open`(ViperOS devfs.vp)写入的是 vtable
|
||||
// 槽位,编译器(TPC HandlesAssign / TransPyV CallGenVt._write_vtable_slot)把
|
||||
// RHS 函数直接 bitcast 成 i8* 存进槽位,**不校验签名与 self**;而 LSP 走普通
|
||||
// 成员赋值路径,用「绑定方法类型」比较,于是 RHS 显式写的 `self: rawptr`
|
||||
// 被当成多余参数 → 误报「参数名称不匹配 / 额外参数」。
|
||||
//
|
||||
// 放行条件:
|
||||
// ① 类是 virtual class,且成员是类里声明的**方法**(函数类型);
|
||||
// ② RHS 是函数且带 self;
|
||||
// ③ RHS 的 self 为泛指针(rawptr / t.CPtr)或该类本身/父类的指针;
|
||||
// ④ 其余参数名称与类型逐个相同、返回类型相同
|
||||
// (`*args` / `**kwargs` 不参与比较,两侧可有无)。
|
||||
function isViperVtableMethodSlotAssignment(
|
||||
classType: ClassType,
|
||||
memberSymbol: Symbol,
|
||||
assignedType: Type
|
||||
): boolean {
|
||||
// classType 可能是**实例**形态(`obj.method = func` 走的是实例成员访问),
|
||||
// 统一转成可实例化形态再判类级别信息(isDerivedFrom 也要求该形态)。
|
||||
const classInstantiable = isInstantiableClass(classType)
|
||||
? classType
|
||||
: ClassType.cloneAsInstantiable(classType);
|
||||
if (!isViperVirtualClass(classInstantiable)) {
|
||||
return false;
|
||||
}
|
||||
const declaredType = getEffectiveTypeOfSymbol(memberSymbol);
|
||||
if (!isFunction(declaredType) || !isFunction(assignedType)) {
|
||||
return false;
|
||||
}
|
||||
const declaredParams = declaredType.shared.parameters;
|
||||
const assignedParams = assignedType.shared.parameters;
|
||||
if (declaredParams.length === 0 || assignedParams.length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (!isViperVtableSelfTypeOk(FunctionType.getParamType(assignedType, 0), classInstantiable)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 收集「除 self 外的非变参参数」,`*args` / `**kwargs` 跳过不比较。
|
||||
const collectRestParams = (fnType: FunctionType) => {
|
||||
const result: { name: string | undefined; type: Type }[] = [];
|
||||
for (let i = 1; i < fnType.shared.parameters.length; i++) {
|
||||
const param = fnType.shared.parameters[i];
|
||||
if (
|
||||
param.category === ParamCategory.ArgsList ||
|
||||
param.category === ParamCategory.KwargsDict
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
result.push({ name: param.name, type: FunctionType.getParamType(fnType, i) });
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const declaredRest = collectRestParams(declaredType);
|
||||
const assignedRest = collectRestParams(assignedType);
|
||||
if (declaredRest.length !== assignedRest.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < declaredRest.length; i++) {
|
||||
if (declaredRest[i].name !== assignedRest[i].name) {
|
||||
return false;
|
||||
}
|
||||
if (!isTypeSame(declaredRest[i].type, assignedRest[i].type)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const declaredReturnType = FunctionType.getEffectiveReturnType(declaredType);
|
||||
const assignedReturnType = FunctionType.getEffectiveReturnType(assignedType);
|
||||
if (!declaredReturnType || !assignedReturnType) {
|
||||
return false;
|
||||
}
|
||||
return isTypeSame(declaredReturnType, assignedReturnType);
|
||||
}
|
||||
|
||||
function getTypeOfClassMemberName(
|
||||
errorNode: ExpressionNode | undefined,
|
||||
classType: ClassType,
|
||||
@@ -7173,30 +7300,38 @@ export function createTypeEvaluator(
|
||||
);
|
||||
|
||||
if (!isDescriptorError && usage.method === 'set' && usage.setType) {
|
||||
if (errorNode && memberInfo.symbol.hasTypedDeclarations()) {
|
||||
// This is an assignment to a member with a declared type. Apply
|
||||
// narrowing logic based on the assigned type. Skip this for
|
||||
// descriptor-based accesses.
|
||||
narrowedTypeForSet = isDescriptorApplied
|
||||
? usage.setType.type
|
||||
: narrowTypeBasedOnAssignment(type, usage.setType).type;
|
||||
}
|
||||
|
||||
// Verify that the assigned type is compatible.
|
||||
if (!assignType(type, usage.setType.type, diag?.createAddendum())) {
|
||||
if (!usage.setType.isIncomplete) {
|
||||
diag?.addMessage(
|
||||
LocAddendum.memberAssignment().format({
|
||||
type: printType(usage.setType.type),
|
||||
name: memberName,
|
||||
classType: printObjectTypeForClass(classType),
|
||||
})
|
||||
);
|
||||
// Viper 扩展:virtual class(vtable)的**方法槽位赋值**(`obj.method = func`)。
|
||||
// 编译器只把 RHS 函数 bitcast 成 i8* 存进 vtable 槽,不校验签名/self,
|
||||
// 故不能按普通成员赋值的「绑定方法类型」比较(见
|
||||
// isViperVtableMethodSlotAssignment)。命中时视为兼容、不做收窄。
|
||||
if (isViperVtableMethodSlotAssignment(classType, memberInfo.symbol, usage.setType.type)) {
|
||||
narrowedTypeForSet = type;
|
||||
} else {
|
||||
if (errorNode && memberInfo.symbol.hasTypedDeclarations()) {
|
||||
// This is an assignment to a member with a declared type. Apply
|
||||
// narrowing logic based on the assigned type. Skip this for
|
||||
// descriptor-based accesses.
|
||||
narrowedTypeForSet = isDescriptorApplied
|
||||
? usage.setType.type
|
||||
: narrowTypeBasedOnAssignment(type, usage.setType).type;
|
||||
}
|
||||
|
||||
// Do not narrow the type in this case. Assume the declared type.
|
||||
narrowedTypeForSet = type;
|
||||
isDescriptorError = true;
|
||||
// Verify that the assigned type is compatible.
|
||||
if (!assignType(type, usage.setType.type, diag?.createAddendum())) {
|
||||
if (!usage.setType.isIncomplete) {
|
||||
diag?.addMessage(
|
||||
LocAddendum.memberAssignment().format({
|
||||
type: printType(usage.setType.type),
|
||||
name: memberName,
|
||||
classType: printObjectTypeForClass(classType),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Do not narrow the type in this case. Assume the declared type.
|
||||
narrowedTypeForSet = type;
|
||||
isDescriptorError = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* viperVtableMethodAssign.test.ts
|
||||
*
|
||||
* 验证 `obj.method = func` 写入 **virtual class(vtable)方法槽位**时的签名校验
|
||||
* (ViperOS devfs.vp 的 `serial0_file.open = serial0_open` 写法)。
|
||||
*
|
||||
* 背景:编译器(TPC HandlesAssign / TransPyV CallGenVt._write_vtable_slot)只把
|
||||
* RHS 函数 bitcast 成 i8* 存进 vtable 槽,**不校验签名与 self**;LSP 若按普通
|
||||
* 成员赋值的「绑定方法类型」比较,RHS 显式写的 `self: rawptr` 会被当成多余参数,
|
||||
* 误报「参数名称不匹配 / 额外参数 …」。
|
||||
*
|
||||
* 放行条件(用户口径):
|
||||
* ① 类是 virtual class,成员是类里声明的方法;
|
||||
* ② RHS 的 self 为泛指针(rawptr / t.CPtr)或该类本身/父类的指针;
|
||||
* ③ 其余参数名称与类型逐个相同、返回类型相同(*args / **kwargs 可有无)。
|
||||
* 否则仍按普通赋值报错(非 virtual class、self 非指针、参数/返回不符等)。
|
||||
*
|
||||
* 运行:npx jest src/tests/viperVtableMethodAssign.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, dirName: 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, dirName);
|
||||
fs.mkdirSync(testDir, { recursive: true });
|
||||
const testFile = path.join(testDir, 'main.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, testDir, serviceProvider };
|
||||
}
|
||||
|
||||
function getDiagnostics(program: Program, configOptions: ConfigOptions, uri: any) {
|
||||
const sourceFile = program.getSourceFile(uri);
|
||||
assert(sourceFile, 'source file not found');
|
||||
return (sourceFile.getDiagnostics(configOptions) || []).map((diag: any) => ({
|
||||
category: diag.category,
|
||||
line: diag.range ? diag.range.start.line + 1 : -1,
|
||||
message: diag.message as string,
|
||||
}));
|
||||
}
|
||||
|
||||
/** 代码里首个包含 snippet 的行号(1-based),用于定位断言。 */
|
||||
function lineOf(code: string, snippet: string): number {
|
||||
return code.split('\n').findIndex((l) => l.includes(snippet)) + 1;
|
||||
}
|
||||
|
||||
const VTABLE_CLASS = `import t
|
||||
|
||||
virtual class devfs_file:
|
||||
private_data: rawptr
|
||||
|
||||
def open(self, path: const str, flags: int) -> int:
|
||||
return 0
|
||||
|
||||
def close(self, fd: int) -> int:
|
||||
return 0
|
||||
|
||||
def read(self, fd: int, buffer: rawptr, size: uint32) -> int:
|
||||
return 0
|
||||
|
||||
def ioctl(self, fd: int, request: int, arg: rawptr, *args) -> int:
|
||||
return 0
|
||||
|
||||
def addref(self, path: const str) -> int:
|
||||
return 0
|
||||
|
||||
def exists(self, path: const str) -> int:
|
||||
return 0
|
||||
|
||||
def create(self, path: const str) -> int:
|
||||
return 0
|
||||
|
||||
def seek(self, fd: int, offset: int) -> int:
|
||||
return 0
|
||||
|
||||
def truncate(self, fd: int, size: uint32) -> int:
|
||||
return 0
|
||||
|
||||
def mmap(self, fd: int, size: uint32) -> int:
|
||||
return 0
|
||||
|
||||
serial0_file: devfs_file
|
||||
|
||||
def serial0_open(self: rawptr, path: const str, flags: int) -> int:
|
||||
return 0
|
||||
|
||||
def serial0_close(self: rawptr, fd: int) -> int:
|
||||
return 0
|
||||
|
||||
def serial0_read(self: rawptr, fd: int, buffer: rawptr, size: uint32) -> int:
|
||||
return 0
|
||||
|
||||
def ok_varargs(self: rawptr, fd: int, request: int, arg: rawptr) -> int:
|
||||
return 0
|
||||
|
||||
def ok_class_ptr(self: devfs_file*, path: const str) -> int:
|
||||
return 0
|
||||
|
||||
def devfs_register_serial0() -> int:
|
||||
global serial0_file
|
||||
serial0_file = devfs_file()
|
||||
serial0_file.open = serial0_open
|
||||
serial0_file.close = serial0_close
|
||||
serial0_file.read = serial0_read
|
||||
serial0_file.ioctl = ok_varargs
|
||||
serial0_file.addref = ok_class_ptr
|
||||
return 0
|
||||
`;
|
||||
|
||||
test('virtual class(vtable)方法槽位赋值:self 为 rawptr / 本身指针 / *args 不同 → 放行', () => {
|
||||
const code = VTABLE_CLASS;
|
||||
const { program, configOptions, uri, testDir, serviceProvider } = buildProgramForCode(code, '__viper_vtable_assign_ok__');
|
||||
|
||||
try {
|
||||
const diagnostics = getDiagnostics(program, configOptions, uri);
|
||||
console.log('=== vtable 槽位赋值(放行)诊断 ===');
|
||||
diagnostics.forEach((d) => console.log(` L${d.line} [cat=${d.category}] ${d.message}`));
|
||||
|
||||
const hard = diagnostics.filter((d) => d.category === 0 || d.category === 1);
|
||||
assert.strictEqual(
|
||||
hard.length,
|
||||
0,
|
||||
`devfs 写法(self: rawptr / self: devfs_file* / *args 不同)不应报错:${JSON.stringify(hard)}`
|
||||
);
|
||||
} finally {
|
||||
program.dispose();
|
||||
serviceProvider.dispose();
|
||||
require('fs').rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('virtual class 方法槽位赋值:self 非指针 / 参数类型·名称·个数·返回不符 → 仍报错', () => {
|
||||
const code = `${VTABLE_CLASS}
|
||||
def bad_self(self: int, fd: int, size: uint32) -> int:
|
||||
return 0
|
||||
|
||||
def bad_param_type(self: rawptr, fd: int, offset: uint32) -> int:
|
||||
return 0
|
||||
|
||||
def bad_param_name(self: rawptr, fd: int, length: uint32) -> int:
|
||||
return 0
|
||||
|
||||
def bad_return(self: rawptr, path: const str) -> uint64:
|
||||
return 0
|
||||
|
||||
def bad_count(self: rawptr, path: const str, flags: int) -> int:
|
||||
return 0
|
||||
|
||||
def register_bad() -> int:
|
||||
global serial0_file
|
||||
serial0_file.mmap = bad_self
|
||||
serial0_file.seek = bad_param_type
|
||||
serial0_file.truncate = bad_param_name
|
||||
serial0_file.exists = bad_return
|
||||
serial0_file.create = bad_count
|
||||
return 0
|
||||
`;
|
||||
const { program, configOptions, uri, testDir, serviceProvider } = buildProgramForCode(code, '__viper_vtable_assign_bad__');
|
||||
|
||||
try {
|
||||
const diagnostics = getDiagnostics(program, configOptions, uri);
|
||||
console.log('=== vtable 槽位赋值(拒绝)诊断 ===');
|
||||
diagnostics.forEach((d) => console.log(` L${d.line} [cat=${d.category}] ${d.message}`));
|
||||
|
||||
const expectedLines = [
|
||||
lineOf(code, 'serial0_file.mmap = bad_self'),
|
||||
lineOf(code, 'serial0_file.seek = bad_param_type'),
|
||||
lineOf(code, 'serial0_file.truncate = bad_param_name'),
|
||||
lineOf(code, 'serial0_file.exists = bad_return'),
|
||||
lineOf(code, 'serial0_file.create = bad_count'),
|
||||
];
|
||||
const hardLines = Array.from(
|
||||
new Set(
|
||||
diagnostics
|
||||
.filter((d) => d.category === 0 || d.category === 1)
|
||||
.map((d) => d.line)
|
||||
)
|
||||
).sort((a, b) => a - b);
|
||||
console.log(`=== 期望报错行 ${JSON.stringify(expectedLines)},实际 ${JSON.stringify(hardLines)} ===`);
|
||||
assert.deepStrictEqual(
|
||||
hardLines,
|
||||
[...expectedLines].sort((a, b) => a - b),
|
||||
`应且仅应报这 5 处:${JSON.stringify(diagnostics.filter((d) => d.category === 0 || d.category === 1))}`
|
||||
);
|
||||
} finally {
|
||||
program.dispose();
|
||||
serviceProvider.dispose();
|
||||
require('fs').rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('非 virtual class(无 vtable)的方法赋值不受放宽:仍报错', () => {
|
||||
const code = `import t
|
||||
|
||||
class plain_file:
|
||||
def open(self, path: const str, flags: int) -> int:
|
||||
return 0
|
||||
|
||||
plain: plain_file
|
||||
|
||||
def plain_open(self: rawptr, path: const str, flags: int) -> int:
|
||||
return 0
|
||||
|
||||
def register_plain() -> int:
|
||||
global plain
|
||||
plain = plain_file()
|
||||
plain.open = plain_open
|
||||
return 0
|
||||
`;
|
||||
const { program, configOptions, uri, testDir, serviceProvider } = buildProgramForCode(code, '__viper_vtable_assign_plain__');
|
||||
|
||||
try {
|
||||
const diagnostics = getDiagnostics(program, configOptions, uri);
|
||||
console.log('=== 非 virtual class 赋值诊断 ===');
|
||||
diagnostics.forEach((d) => console.log(` L${d.line} [cat=${d.category}] ${d.message}`));
|
||||
|
||||
const targetLine = lineOf(code, 'plain.open = plain_open');
|
||||
const hardOnLine = diagnostics.filter(
|
||||
(d) => (d.category === 0 || d.category === 1) && d.line === targetLine
|
||||
);
|
||||
assert(
|
||||
hardOnLine.length > 0,
|
||||
`非 virtual class 的方法赋值不应被放行(应仍报错):${JSON.stringify(diagnostics)}`
|
||||
);
|
||||
} finally {
|
||||
program.dispose();
|
||||
serviceProvider.dispose();
|
||||
require('fs').rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -42,6 +42,7 @@
|
||||
- Attribute names inside `__attribute__(...)` are highlighted **structurally**, so a name that the compiler supports but that isn't declared in the `attr` class still colours like the declared ones. `stackrealign` (supported by TransPyV's code generator — `FuncGenBase.vp` maps it to the LLVM `"stackrealign"` attribute — and used by ViperOS `VKernel/Kernel/main.vp`) is declared in neither `t.py` nor the `t.vpi` stub, so `t.attr.stackrealign` used to be unresolvable: the name got no semantic token (rendered unhighlighted) and the file was additionally flagged with "无法访问类"type[attr]"的属性"stackrealign"". The parser now marks the synthesized attribute-name node, the semantic token provider colours it as `function` (exactly like a declared attribute such as `section` / `aligned` / `naked`), and the member-access diagnostic is suppressed for these names — the compiler silently drops unknown attributes rather than rejecting them, so reporting them was a false positive. Declared names keep resolving through `t.attr` as before, so hover and jump-to-definition still work for them
|
||||
- `__attribute__(...)` **completion and hover** now list attribute names that the compiler supports but that the `attr` class doesn't declare, instead of only mirroring `t.attr`. `stackrealign` is offered by typing inside the parentheses and appears in the hover list of available names; the list is a single shared union (`viperAttributeUtils.getViperAttributeNames`) of `t.attr`'s declared names plus these extra names, so completion and hover cannot drift apart. Declared names keep coming from the stub symbol table (so they still show their signature and support jump-to-definition)
|
||||
- Pointer-to-pointer implicit conversion is now **strict** instead of "same level is always allowed". A pointer converts implicitly to another pointer only when the **level** matches **and** the pointee is compatible: either side being `void*` / undetermined (`rawptr`) always allows it, identical pointees are allowed, two basic (numeric scalar) pointees follow the very same rules as the values themselves (`uint32* → uint64*` and `uint32* → uint16*` are allowed because integers convert in every direction; `float* → int*` is still reported), and for **structs** only an **upcast** is allowed (`Sub* → Node*`; `memhub` / `requests` depend on `MemBuddy* → MemManager*`), while two unrelated structs (`Node* → Other*`), a downcast, or a struct↔basic mix (`Node* → uint64*`) are rejected. Single-level pointers also convert to and from `void*` (`t.CVoid`) in both directions, while multi-level ones still don't (`Node** → void*` is reported). `&x` / `*x` already produced the right types (`&x` is `T*` for `x: T`, `T**` for `x: T*`; `*x` is `T` for `x: T*`) and now take part in these checks too — `a(&x)` with `x: Node*` (a `Node**` into a `void*` parameter) is reported as before, and so is `a(&x)` where the parameter is a single-level pointer
|
||||
- Assigning a **plain function to a method slot of a `virtual class`** is now accepted: `serial0_file.open = serial0_open` (ViperOS `devfs.vp`, where `devfs_file` is a `virtual class` and `serial0_open(self: rawptr, path: const str, flags: int) -> int` is a module-level function) used to be reported as "无法为类"devfs_file"的属性"open"赋值 / 参数名称不匹配: "path" versus "self" / 额外参数 "flags"". Such an assignment writes a **vtable slot**, and the compiler (TPC `HandlesAssign` / TransPyV `CallGenVt._write_vtable_slot`) just bitcasts the function to `i8*` and stores it — it never validates the signature or `self` — whereas the language server compared the *bound* method type (with `self` already stripped), so the explicitly written `self` looked like a spurious extra parameter. The check now follows the vtable semantics: the class must be a `virtual class` (`virtual class X` or `@t.CVTable class X`), the member must be a method declared in it, the assigned function's `self` must be a generic pointer (`rawptr` / `t.CPtr`) or a pointer to that class itself or one of its base classes, and every remaining parameter must match by name and type with an identical return type (`*args` / `**kwargs` are not compared, so they may be present on either side). Everything else — a non-virtual class, `self` that isn't such a pointer, or a parameter/return mismatch — is still reported
|
||||
|
||||
## 1.1.425
|
||||
|
||||
|
||||
Reference in new issue
Block a user