diff --git a/.gitignore b/.gitignore index 8d9b8455..cb4c53a4 100644 --- a/.gitignore +++ b/.gitignore @@ -152,6 +152,8 @@ packages/vipeye-internal/src/tests/__viper_static__/ packages/vipeye-internal/src/tests/__viper_bracket_array_expr__/ packages/vipeye-internal/src/tests/__viper_type_binop_bound__/ packages/vipeye-internal/src/tests/__viper_const_ptr_deref__/ +packages/vipeye-internal/src/tests/__viper_const_ptr_index__/ +packages/vipeye-internal/src/tests/__viper_const_ptr_iter__/ # perfCompare.py cached commit bundles /build/perfCompare/ diff --git a/packages/vipeye-internal/src/analyzer/typeEvaluator.ts b/packages/vipeye-internal/src/analyzer/typeEvaluator.ts index 59b5aeb8..f3bbf041 100644 --- a/packages/vipeye-internal/src/analyzer/typeEvaluator.ts +++ b/packages/vipeye-internal/src/analyzer/typeEvaluator.ts @@ -3300,10 +3300,8 @@ export function createTypeEvaluator( if (ClassType.isBuiltIn(subtype, ['str', 'bytes'])) { return getBuiltInObject(errorNode, 'char'); } - // 检查联合类型中是否包含 CPtr - const typeContainsCPtr = findSubtype(type, (s) => - isClassInstance(s) && isCPtrType(s)) !== undefined; - if (typeContainsCPtr && ( + // 检查联合类型中是否包含 Viper 指针(CPtr / str / bytes) + if (viperTypeContainsPointer(type) && ( isCConstType(subtype) || isCStaticType(subtype) || isCVolatileType(subtype) || isCVoidType(subtype) || ClassType.isBuiltIn(subtype, 'int') @@ -3453,9 +3451,11 @@ export function createTypeEvaluator( } return UnknownType.create(); } - const typeContainsCPtr = findSubtype(type, (s) => - isClassInstance(s) && isCPtrType(s)) !== undefined; - if (typeContainsCPtr && ( + // 判据必须认全 Viper 指针(CPtr / str / bytes),不能只认 CPtr: + // `path: const str`(AST = `CConst | str`)里的指针成员是 str, + // 旧写法 findSubtype(isCPtrType) 匹配不上,CConst 落到末尾 + // 报「“CConst”不可迭代 / 未定义 __iter__」。 + if (viperTypeContainsPointer(type) && ( isCConstType(subtype) || isCStaticType(subtype) || isCVolatileType(subtype) || isCVoidType(subtype) || ClassType.isBuiltIn(subtype, 'int') @@ -4837,6 +4837,23 @@ export function createTypeEvaluator( case ParseNodeType.Index: { const baseTypeResult = getTypeOfExpression(target.d.leftExpr, EvalFlags.IndexBaseDefaults); + // Viper 扩展:const 指针(如 `lfn: const str` = `CConst | str`)指向只读 + // 数据 —— 下标**读**允许,下标**赋值**(`lfn[i] = c`)必须报错, + // 与解引用赋值(`*lfn = c`)共用同一诊断。 + if ( + IsViperDialect(AnalyzerNodeInfo.getFileInfo(target).executionEnvironment.dialect) && + isViperConstPointerType(baseTypeResult.type) + ) { + addDiagnostic( + DiagnosticRule.reportGeneralTypeIssues, + LocMessage.viperConstDerefAssignment().format({ + type: printType(baseTypeResult.type), + }), + target + ); + break; + } + getTypeOfIndexWithBaseType( target, baseTypeResult, @@ -9554,6 +9571,19 @@ export function createTypeEvaluator( return hasConstModifier && hasPointer; } + // Viper 扩展:联合类型里是否存在「Viper 指针」子类型(CPtr[T] / str / bytes)。 + // str / bytes 在 Viper 里就是 C 的 i8*(char*),与 CPtr 同为指针; + // 迭代 / 解引用等路径据此判断「联合里的 C 修饰符(CConst 等)只是修饰,可跳过」, + // 而 `const int`(CConst | int,无指针成员)仍按不可迭代/不可解引用报错。 + function viperTypeContainsPointer(type: Type): boolean { + return ( + findSubtype( + type, + (s) => isClassInstance(s) && (isCPtrType(s) || ClassType.isBuiltIn(s, ['str', 'bytes'])) + ) !== undefined + ); + } + // Viper 扩展:解析 `t.attr.` / `t.attr.(...)` 属性标记 → t.CAttr 实例。 // // 形态(与 TransPyC HandlesAssign._TAttrName / TransPyV ClassGenBase._tattr_name diff --git a/packages/vipeye-internal/src/tests/viperBracketArrayAndConstPtr.test.ts b/packages/vipeye-internal/src/tests/viperBracketArrayAndConstPtr.test.ts index b4af1dfd..f30e2250 100644 --- a/packages/vipeye-internal/src/tests/viperBracketArrayAndConstPtr.test.ts +++ b/packages/vipeye-internal/src/tests/viperBracketArrayAndConstPtr.test.ts @@ -15,8 +15,12 @@ * 边界:两侧不是整数的运算(如类名相加)仍须报错。 * ② `const` 指针(如 `lfn: const str` = `CConst | str`)指向只读数据: * 解引用**读**允许(`*lfn`、`lfn[0]`); - * 解引用**赋值**必须报错(`*lfn = c`); - * 非 const 指针(`buf: str`)的 `*buf = c` 仍合法。 + * 解引用**赋值**必须报错(`*lfn = c`、`lfn[i] = c`); + * 非 const 指针(`buf: str`)的 `*buf = c` / `buf[i] = c` 仍合法。 + * ③ C 修饰符(CConst / CStatic / CVolatile / CVoid)在迭代 / 解引用路径上 + * **只是修饰**,跳过即可:`path: const str` 可 `for ch in path:` 迭代, + * 不再误报「“CConst”不可迭代 / 未定义 __iter__」(判据须认 str / bytes, + * 不能只认 CPtr,见 viperTypeContainsPointer)。 * * 运行:npx jest src/tests/viperBracketArrayAndConstPtr.test.ts --runInBand */ @@ -206,3 +210,91 @@ def mutable_ok(buf: str) -> None: require('fs').rmSync(testDir, { recursive: true, force: true }); } }); + +test('const 指针:下标读允许、下标赋值报错;非 const 指针下标赋值仍合法', () => { + const code = `def index_read(lfn: const str) -> char: + return lfn[0] + +def index_write(lfn: const str) -> None: + lfn[0] = char(65) + +def mutable_index_ok(buf: str) -> None: + buf[0] = char(65) +`; + const { program, configOptions, uri, serviceProvider, testDir } = buildProgramForCode( + code, + '__viper_const_ptr_index__' + ); + + try { + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== const 指针下标诊断 ==='); + diagnostics.forEach((d) => console.log(` L${d.line}:${d.col} [category=${d.category}] ${d.message}`)); + + const constWriteDiags = diagnostics.filter((d) => d.message.includes('const 指针的解引用赋值')); + assert.strictEqual( + constWriteDiags.length, + 1, + `只应报 1 处 const 下标赋值错误(lfn[0] = ...):${JSON.stringify(diagnostics)}` + ); + // 报错行应为 `lfn[0] = char(65)` 所在行(第 5 行)。 + assert.strictEqual( + constWriteDiags[0].line, + 5, + `const 下标赋值错误应落在第 5 行:${JSON.stringify(constWriteDiags)}` + ); + + const otherErrors = diagnostics.filter( + (d) => d.category === 0 && !d.message.includes('const 指针的解引用赋值') + ); + assert.strictEqual( + otherErrors.length, + 0, + `下标读 / 非 const 指针下标赋值不应报错:${JSON.stringify(otherErrors)}` + ); + } finally { + program.dispose(); + serviceProvider.dispose(); + require('fs').rmSync(testDir, { recursive: true, force: true }); + } +}); + +test('const 指针可迭代:for ch in path 不报「不可迭代 / 未定义 __iter__」', () => { + const code = `import t + +def walk(path: const str) -> int: + count: int = 0 + for ch in path: + count = count + 1 + return count + +def walk_parts(parts: const t.CPtr) -> int: + total: int = 0 + for p in parts: + total = total + 1 + return total +`; + const { program, configOptions, uri, serviceProvider, testDir } = buildProgramForCode( + code, + '__viper_const_ptr_iter__' + ); + + try { + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== const 指针迭代诊断 ==='); + diagnostics.forEach((d) => console.log(` L${d.line}:${d.col} [category=${d.category}] ${d.message}`)); + + const notIterableDiags = diagnostics.filter( + (d) => d.message.includes('不可迭代') || d.message.includes('__iter__') + ); + assert.strictEqual( + notIterableDiags.length, + 0, + `const 指针(const str 等)应可迭代,不应报不可迭代:${JSON.stringify(diagnostics)}` + ); + } finally { + program.dispose(); + serviceProvider.dispose(); + require('fs').rmSync(testDir, { recursive: true, force: true }); + } +}); diff --git a/packages/vscode-vipeye/CHANGELOG.md b/packages/vscode-vipeye/CHANGELOG.md index 8113af2a..3d5da67b 100644 --- a/packages/vscode-vipeye/CHANGELOG.md +++ b/packages/vscode-vipeye/CHANGELOG.md @@ -32,7 +32,8 @@ - The **`static` modifier** is now supported (LSP side only; TransPyC/TransPyV are unchanged). It is a *soft* keyword: `static def f(...)` / `static fn f(...)` marks a function (recorded on `FunctionNode.d.viperModifier`, exactly like `virtual`) and `static x: T = v` marks a variable (the annotation is rewritten to `t.CStatic | T`, the same tree TransPyC's `_parse_type_ann` builds by inserting `t.CStatic` at the left of the union, so `static SECRET: int` renders as `static int`). Anywhere else `static` stays an ordinary identifier — `static = 1`, `static: int`, `static(x)`, `def f(static: int)` and `self.static` all parse as before. Its meaning is C's **internal linkage**: a static symbol **cannot be used across modules**. Referencing one from another module is reported as `'lib.helper' 是 static 符号,禁止跨模块使用(static 仅限定义模块内可见)` for both `mod.name` / `mod.fn()` (checked in the module member-access path) and `from mod import name` (checked on the import statement), while same-module references are allowed — the decision is made per declaration file, so a self-import is not flagged either. `static` is also offered by completion and highlighted as a modifier (`storage.modifier.viper`, matched only in front of `def`-family or `name :`) - The `f` prefix of an f-string is now highlighted as `storage.type.string.viper` (blue in the default dark theme) instead of being left unstyled. The rule matches the `r` / `u` / `b` / `f` prefix combination immediately preceding a quote, so it covers `f"..."`, `f'''...'''` and mixed spellings like `rf"..."`; a bare `r`/`f`/`b` identifier not followed by a quote is unaffected, and the string body keeps the usual `string.quoted.*` scope - The **size of a fixed-length array type may be any integer constant expression** now: `part_types: [MAX_VOLUMES * MAX_PARTITIONS]uint8`, `part_lit: [MAX_VOLUMES * 2]uint8`, `part_left: [MAX_VOLUMES - 1]uint8` and `part_shift: [1 << 3]uint8` all parse and check cleanly. Two bugs made this fail before: the parser parsed the `[SIZE]` part while still flagged as a type annotation, so `_parseArithmeticTerm` deliberately stopped at the `*` (it is the C pointer suffix marker there) — the bracket was reported as "“[" 未关闭" and the leftover `* MAX_PARTITIONS]uint8` as "类型表达式中不允许使用解包运算符"; and the type evaluator then evaluated that arithmetic with `EvalFlags.TypeExpression`, reporting "类型表达式中不允许使用二元运算符". The size is now parsed as the value expression it is (the flag is cleared for its duration), and the evaluator allows arithmetic/bitwise operators in a type expression when **both operands are Viper integers** (the case the enclosing comment already described, e.g. `t.CArray[T, MAX_ORDER + 1]`); non-integer operands (`C1 + C2`) are still reported -- **`const` pointers are read-only for dereference assignment**: `lfn: const str` (`CConst | str`, i.e. `const char *`) now allows dereferencing — `*lfn` and `lfn[0]` evaluate to `char` — but writing through the dereference (`*lfn = char(65)`) is reported as "不能给 const 指针的解引用赋值("const str" 指向的数据为只读)". Previously the dereference itself was rejected with a bogus "类型"const str"不支持运算符"*"" (the `CConst` modifier in the union was mistaken for a non-pointer operand), and there was no read-only check at all. A non-`const` pointer (`buf: str`) still accepts `*buf = char(65)` +- **`const` pointers are read-only for dereference assignment**: `lfn: const str` (`CConst | str`, i.e. `const char *`) now allows dereferencing — `*lfn` and `lfn[0]` evaluate to `char` — but writing through the dereference is reported as "不能给 const 指针的解引用赋值("const str" 指向的数据为只读)". This covers **both** spellings, `*lfn = char(65)` (dereference) and `lfn[0] = char(65)` (**subscript assignment**), which share the same diagnostic. Previously the dereference itself was rejected with a bogus "类型"const str"不支持运算符"*"" (the `CConst` modifier in the union was mistaken for a non-pointer operand), and there was no read-only check at all. A non-`const` pointer (`buf: str`) still accepts `*buf = char(65)` and `buf[0] = char(65)` +- C type descriptors are **skipped** when checking iterability, so a `const` pointer can be iterated: `for ch in path:` with `path: const str` (and `const t.CPtr`) no longer reports "“CConst”不可迭代 / 未定义“__iter__”方法". The predicate now recognizes every Viper pointer (CPtr, `str`, `bytes`) rather than only `CPtr`, which is why `const str` had been missed — `CConst` fell through to the "not iterable" path ## 1.1.425