feat(viper): VET 未启用时仍可校验/选择编译器(TS 模拟 check)

手动「选择编译器…」原先只能走 VET 的 check 通道(stdin 写
{"check": path, "id": N} → stdout 单行回执),该通道依附常驻进程与心跳看门狗:
VET 被禁用 / 手动停止 / 启动失败 / confirmSeconds=0 时选编译器直接失败。

现在 EnvironmentTool.CheckExecutable 先看 CanUseCheckChannel()(要求
state === 'running' && confirmSeconds > 0 && 进程在 && stdin 可写;启动中/重启中
也走本地路径,否则要等它扫完一轮才可能进确认窗口、白等超时),不满足即回退
本地模拟:

* 新增 packages/viper/src/executableProbe.ts(LocalCheckExecutable):
  ① 读 PE:DOS MZ → e_lfanew → PE\0\0 → 可选头 Magic → 节表 → .PRVERSN 段,
     取版本串;COFF Machine → arch。小端字段一律用逐字节「或 + 移位」位操作解码
     (与 VET VPe.ReadU16/ReadU32 同口径)。
  ② 跑 <exe> --version:合并捕获 stdout+stderr、5 秒超时(= VET 的
     VERSION_PROBE_TIMEOUT_MS)、输出前 5 字符须为 Viper。
* 失败原因串与 VET 的 check_msg 逐字一致(NOT_FOUND / NO_PERMISSION / NOT_EXE /
  NO_PRVERSN / PRVERSN_ERR / PREFIX_ERR / TIMEOUT / EXE_ERR,0x<HEX>),故文案与
  选择/回报流程完全复用;本地路径只读,不写缓存、不入库。
* VET 处于 running 时行为不变(仍由 VET 校验并写回它的缓存)。

配套文案:进度标题与提示去掉「VET」前缀(正在检查… / 检查未通过:…);
状态栏 tooltip 增加「VET 未运行,选择时由扩展本地检查该 exe」说明。
This commit is contained in:
TermiNexus committed 2026-10-02 18:57:40 +08:00
1 parent 3fd61c2594
commit 4c30604ecd
5 files changed
+415 -15

No files matched your search

+31 -2
View File
@@ -20,6 +20,7 @@ import * as fs from 'fs';
import * as path from 'path';
import { commands, Disposable, Event, EventEmitter, OutputChannel, window, workspace } from 'vscode';
import { LocalCheckExecutable } from './executableProbe';
import { ViperCheckResult, ViperEnvironment } from './types';
/**
@@ -136,7 +137,35 @@ export class EnvironmentTool implements Disposable {
}
/**
* 通过 VET 的 check 通道检查一个 exe(用户手动指定位置时用)。
* 检查一个 exe 是否可用作 Viper 编译器(用户手动指定位置时用)。
*
* 优先走 VET 的 check 通道;**VET 未启用/未运行/心跳关闭时回退到本地模拟**
* (`executableProbe.LocalCheckExecutable`:读 PE/.PRVERSN + `<exe> --version` 带超时),
* 两条路径的通过判据与失败原因串完全一致,调用方无需区分。
*/
CheckExecutable(exePath: string): Promise<ViperCheckResult> {
if (this.CanUseCheckChannel()) {
return this.CheckViaVet(exePath);
}
this.AppendLog(
`[vscode] VET check 通道不可用(${DescribeStateForMessage(this.state)}),改用本地检查: ${exePath}`
);
return LocalCheckExecutable(exePath, (line) => this.AppendLog(line));
}
/**
* check 通道是否可用:需要 VET 处于 **running**(启动中/重启中的请求要等它扫完一轮
* 才可能进确认窗口,等不到回执就会白等超时)、开了心跳(看门狗是请求的转发方)、
* 且 stdin 仍可写。其余情形一律走本地模拟,保证选编译器随时可用。
*/
private CanUseCheckChannel(): boolean {
const config = this.ReadConfig();
return this.state === 'running' && config.confirmSeconds > 0 && !!this.process && this.process.stdin.writable;
}
/**
* 通过 VET 的 check 通道检查一个 exe。
*
* 通道形态:请求从 **stdin** 写入一行 `{"check": "<绝对路径>", "id": <整数>}`,
* 由看门狗(confirm-watch)识别后经匿名管道转发给 scan;回执是 **stdout** 的一行 JSON,
@@ -144,7 +173,7 @@ export class EnvironmentTool implements Disposable {
*
* 通过后 VET 会把该项入库并写入缓存文件,下一轮(≤ scanIntervalMs)快照里就会出现它。
*/
CheckExecutable(exePath: string): Promise<ViperCheckResult> {
private CheckViaVet(exePath: string): Promise<ViperCheckResult> {
const config = this.ReadConfig();
if (config.confirmSeconds <= 0) {
return Promise.reject(
+358
View File
@@ -0,0 +1,358 @@
/*
* executableProbe.ts
* VET 未启用时的**本地**编译器校验(VET check 通道的 TypeScript 模拟)。
*
* 背景:手动「选择编译器…」原先只能经 VET 的 check 通道(stdin 请求 → stdout 回执),
* 而该通道依附常驻进程与心跳看门狗 —— VET 被禁用 / 手动停止 / 启动失败 / 未开
* confirmSeconds 时,选编译器直接失败。本模块用 TS 复刻同一条口径,使 VET 缺席时仍可选用:
* ① 读 PE:DOS/PE 头 → 节表 → `.PRVERSN` 段 → 版本串;COFF Machine → arch
* (与 VET/App/lib/VPe.vp 的 TryReadPeInfo + ParsePeHead 逐条同口径,小端读取用
* 逐字节"或 + 移位"的位操作,不用 Buffer 的 LE 读取)
* ② 执行 `<exe> --version`:合并捕获 stdout+stderr,**5 秒超时**(与 VProc.RunVersionProbe
* 的 VERSION_PROBE_TIMEOUT_MS 一致),输出前 5 字符必须是 "Viper"
*
* 失败原因串与 VET 的 check_msg **逐字一致**(NOT_FOUND / NO_PERMISSION / NOT_EXE /
* NO_PRVERSN / PRVERSN_ERR / PREFIX_ERR / TIMEOUT / EXE_ERR,0x<HEX>),故调用方沿用
* 同一套文案(extension.DescribeCheckMessage)即可,不必区分"校验来自 VET 还是本地"。
*
* 只读不写:本地检查不落盘、不入库(VET 会把它写进自己的缓存文件,这里没有缓存)。
*/
import { ChildProcess, spawn } from 'child_process';
import { randomUUID } from 'crypto';
import * as fs from 'fs';
import { ViperCheckResult } from './types';
// ===== PE/COFF 常量(与 VET/App/lib/VPe.vp 一一对应) =====
const DosSignature = 0x5a4d; // "MZ" 小端
const NtSignature = 0x00004550; // "PE\0\0" 小端
const OptMagicPe32 = 0x10b;
const OptMagicPe32Plus = 0x20b;
const SecEntrySize = 40;
const ELfanewOffset = 0x3c; // DOS 头 e_lfanew
const CoffMachineOffset = 4; // PE签名(4) + COFF: Machine @ COFF+0
const CoffNumSectionsOffset = 6; // PE签名(4) + COFF: NumberOfSections @ COFF+2
const CoffOptSizeOffset = 20; // PE签名(4) + COFF: SizeOfOptionalHeader @ COFF+16
const OptMagicOffset = 24; // PE签名(4) + COFF(20) 之后为可选头 Magic
const SecNameOffset = 0;
const SecRawSizeOffset = 16;
const SecRawPtrOffset = 20;
const VersionSectionName = '.PRVERSN';
const PeHeadRead = 8192; // 头部窗口(DOS + COFF + 可选头 + 节表),与 VPe.PE_HEAD_READ 同值
const VersionCap = 128; // .PRVERSN 版本串上限(VPe.VER_CAP)
const VersionProbeCap = 4096; // --version 捕获上限(VProc.VERSION_PROBE_DIAG_CAP)
const VersionProbeTimeoutMs = 5000; // --version 探测超时(VProc.VERSION_PROBE_TIMEOUT_MS)
const VersionPrefix = 'Viper'; // --version 输出前缀(VProc.VERSION_PREFIX)
/** check 失败原因串(与 VET VCheck 的 CK_* 常量逐字一致)。 */
export const CheckReason = {
NotFound: 'NOT_FOUND',
NoPermission: 'NO_PERMISSION',
NotExe: 'NOT_EXE',
NoPrversn: 'NO_PRVERSN',
PrversnErr: 'PRVERSN_ERR',
PrefixErr: 'PREFIX_ERR',
Timeout: 'TIMEOUT',
} as const;
// ===== 小端读取(逐字节位操作,与 VPe.ReadU16 / ReadU32 同口径) =====
/** 小端读 u16。 */
function ReadU16(buf: Buffer, offset: number): number {
return buf[offset] | (buf[offset + 1] << 8);
}
/** 小端读 u32(`>>> 0` 归为无符号,避免最高位为 1 时按有符号数参与比较)。 */
function ReadU32(buf: Buffer, offset: number): number {
return (buf[offset] | (buf[offset + 1] << 8) | (buf[offset + 2] << 16) | (buf[offset + 3] << 24)) >>> 0;
}
/** COFF Machine → arch 字符串(与 VPe.FillArch 的不同取值分支一致)。 */
function MachineToArch(machine: number): string {
switch (machine) {
case 0x8664:
return 'x86_64';
case 0x14c:
return 'x86';
case 0xaa64:
return 'arm64';
case 0x1c0:
case 0x1c4:
return 'arm';
default:
// 与 VPe 的 snprintf("unknown(0x%X)") 同格式:大写十六进制、不补零
return `unknown(0x${machine.toString(16).toUpperCase()})`;
}
}
/**
* PE 解析失败码 → 固定原因串(与 VET VCheck.MapPeFailure 同口径)。
* 本模块只发出 rc=6/7(.PRVERSN 段越界/读取失败)与 100+prc(ParsePeHead 的解析码)。
*/
function MapPeFailure(rc: number): string {
if (rc === 6 || rc === 7) {
return CheckReason.PrversnErr;
}
if (rc >= 100) {
const parseRc = rc - 100;
if (parseRc === 7) {
return CheckReason.NoPrversn; // 未找到 .PRVERSN
}
if (parseRc === 6) {
return CheckReason.PrversnErr; // 节表超出头部窗口(无法定位段)
}
}
return CheckReason.NotExe;
}
/** 打开失败 → NOT_FOUND / NO_PERMISSION(与 VET 的 OpenErr 口径:找不到文件才算 NOT_FOUND,其余归无权限)。 */
function MapOpenFailure(error: unknown): string {
return (error as NodeJS.ErrnoException).code === 'ENOENT' ? CheckReason.NotFound : CheckReason.NoPermission;
}
/** 取段内容里到第一个 NUL 为止的版本串(长度上限 VersionCap-1 = 127,与 VPe.CopyVersion 一致)。 */
function CopyVersion(buf: Buffer, offset: number, length: number): string {
let n = 0;
while (n < length && n < VersionCap - 1) {
if (buf[offset + n] === 0) {
break;
}
n++;
}
return buf.toString('utf8', offset, offset + n);
}
/** 从头部窗口解析 PE:节表里找 `.PRVERSN`,取段位置(raw_ptr / raw_size)与 arch。 */
function ParsePeHead(
head: Buffer,
got: number
): { Ok: true; RawPtr: number; RawSize: number; Arch: string } | { Ok: false; Message: string } {
// 各失败码与 VPe.ParsePeHead 的 prc 编码一致(101=窗口不足 … 107=未找到 .PRVERSN)
if (got < 0x40) {
return { Ok: false, Message: MapPeFailure(101) };
}
if (ReadU16(head, 0) !== DosSignature) {
return { Ok: false, Message: MapPeFailure(102) };
}
const lfanew = ReadU32(head, ELfanewOffset);
if (lfanew + 24 > got) {
return { Ok: false, Message: MapPeFailure(103) };
}
if (ReadU32(head, lfanew) !== NtSignature) {
return { Ok: false, Message: MapPeFailure(104) };
}
const numSections = ReadU16(head, lfanew + CoffNumSectionsOffset);
const optSize = ReadU16(head, lfanew + CoffOptSizeOffset);
const magic = ReadU16(head, lfanew + OptMagicOffset);
if (magic !== OptMagicPe32 && magic !== OptMagicPe32Plus) {
return { Ok: false, Message: MapPeFailure(105) };
}
const machine = ReadU16(head, lfanew + CoffMachineOffset);
const sectionTableOffset = lfanew + 24 + optSize;
if (sectionTableOffset + numSections * SecEntrySize > got) {
return { Ok: false, Message: MapPeFailure(106) };
}
for (let index = 0; index < numSections; index++) {
const entry = sectionTableOffset + index * SecEntrySize;
// 节名恰为 8 字节的 ".PRVERSN"(与 VPe 的 strncmp(name, ".PRVERSN", 8) 等价)
const name = head.toString('latin1', entry + SecNameOffset, entry + SecNameOffset + 8);
if (name === VersionSectionName) {
return {
Ok: true,
RawPtr: ReadU32(head, entry + SecRawPtrOffset),
RawSize: ReadU32(head, entry + SecRawSizeOffset),
Arch: MachineToArch(machine),
};
}
}
return { Ok: false, Message: MapPeFailure(107) };
}
/**
* 读一个 exe 的 `.PRVERSN` 版本串与 arch(check 的第 ① 步)。
* 成功返回版本串 + arch;失败返回固定原因串(可直接作为 check_msg 展示)。
*
* 与 VET 的差别只在"段内容不在头部窗口时怎么拿":VET 因 Win32 seek 传参缺陷改走整文件读,
* 这里按段位置定位读(得到同一段字节,且不必把上百 MB 的 exe 读进内存)。
*/
function ReadPeVersionInfo(
exePath: string
): { Ok: true; Version: string; Arch: string } | { Ok: false; Message: string } {
let fd: number;
try {
fd = fs.openSync(exePath, 'r');
} catch (error) {
return { Ok: false, Message: MapOpenFailure(error) };
}
try {
const fileSize = fs.fstatSync(fd).size;
if (fileSize < 0x40) {
return { Ok: false, Message: CheckReason.NotExe };
}
const headCap = Math.min(PeHeadRead, fileSize);
const head = Buffer.alloc(headCap);
const got = fs.readSync(fd, head, 0, headCap, 0);
if (got < 0x40) {
// 文件在"取大小"与"读头部"之间被改小等极端情形:读不出头部即非有效 PE
// (VET 用 rc=5,其 MapPeFailure 归到 NO_PRVERSN;这里按语义归 NOT_EXE)
return { Ok: false, Message: CheckReason.NotExe };
}
const parsed = ParsePeHead(head, got);
if (!parsed.Ok) {
return { Ok: false, Message: parsed.Message };
}
const { RawPtr, RawSize, Arch } = parsed;
if (RawSize === 0 || RawPtr + RawSize > fileSize) {
return { Ok: false, Message: MapPeFailure(6) };
}
let section = head;
let sectionOffset = RawPtr;
if (RawPtr + RawSize > got) {
section = Buffer.alloc(RawSize);
const readBytes = fs.readSync(fd, section, 0, RawSize, RawPtr);
if (readBytes < RawSize) {
return { Ok: false, Message: MapPeFailure(7) };
}
sectionOffset = 0;
}
const version = CopyVersion(section, sectionOffset, RawSize);
if (version.length === 0) {
// 段在但内容为空(首字节即 NUL)——与 VET CheckExecutable 的判据一致
return { Ok: false, Message: CheckReason.PrversnErr };
}
return { Ok: true, Version: version, Arch };
} catch {
return { Ok: false, Message: CheckReason.NoPermission };
} finally {
try {
fs.closeSync(fd);
} catch {
// 关闭失败无关紧要(进程会释放句柄)
}
}
}
/** 拉起失败 → `EXE_ERR,0x<大写十六进制>`(与 VET VCheck.FormatExeErr 同格式)。 */
function FormatExeErr(error: unknown): string {
const err = error as NodeJS.ErrnoException;
let code = 0;
if (err.code === 'ENOENT') {
code = 2; // Win32 ERROR_FILE_NOT_FOUND
} else if (err.code === 'EACCES' || err.code === 'EPERM') {
code = 5; // Win32 ERROR_ACCESS_DENIED
} else if (typeof err.errno === 'number' && err.errno > 0) {
code = err.errno;
}
return `EXE_ERR,0x${code.toString(16).toUpperCase()}`;
}
/**
* 执行 `<exe> --version`(check 的第 ② 步):合并捕获 stdout+stderr、5 秒超时、
* 输出前 5 字符必须是 "Viper"。
*
* 与 VET 的差别:VET 把 stdout 与 stderr 接到**同一个**管道;Node 做不到,故按到达顺序
* 追加到同一缓冲。只判前 5 个字符,两种接法结论一致。
*/
function ProbeVersion(exePath: string): Promise<{ Ok: true } | { Ok: false; Message: string }> {
let child: ChildProcess;
try {
child = spawn(exePath, ['--version'], { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
} catch (error) {
return Promise.resolve({ Ok: false, Message: FormatExeErr(error) });
}
return new Promise((resolve) => {
let output = '';
let settled = false;
let timedOut = false;
const Finish = (result: { Ok: true } | { Ok: false; Message: string }): void => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
resolve(result);
};
const timer = setTimeout(() => {
timedOut = true;
try {
child.kill();
} catch {
// 进程可能已退出:忽略
}
Finish({ Ok: false, Message: CheckReason.Timeout });
}, VersionProbeTimeoutMs);
const Capture = (chunk: Buffer): void => {
if (output.length >= VersionProbeCap) {
return;
}
output = (output + chunk.toString('utf8')).slice(0, VersionProbeCap);
};
child.stdout?.on('data', Capture);
child.stderr?.on('data', Capture);
child.on('error', (error) => Finish({ Ok: false, Message: FormatExeErr(error) }));
child.on('close', () => {
if (timedOut) {
return; // 超时已回执,忽略随后的 close
}
Finish(output.startsWith(VersionPrefix) ? { Ok: true } : { Ok: false, Message: CheckReason.PrefixErr });
});
});
}
/**
* 本地校验一个编译器:读 PE/.PRVERSN + `<exe> --version`(VET check 通道的 TS 模拟)。
* 失败原因串与 VET 的 check_msg 一致,调用方可直接用同一套文案解释。
* 返回的 Id 固定 0:本地检查不经"请求 id / 回执配对"。
*
* @param log 可选的日志出口(接进 "Vipeye VET" 输出通道),用于留下"这一步做了什么"。
*/
export async function LocalCheckExecutable(
exePath: string,
log?: (line: string) => void
): Promise<ViperCheckResult> {
const pe = ReadPeVersionInfo(exePath);
if (!pe.Ok) {
log?.(`[probe] PE 检查未通过: ${pe.Message}`);
return { Ok: false, Id: 0, Message: pe.Message };
}
const probe = await ProbeVersion(exePath);
if (!probe.Ok) {
log?.(`[probe] --version 检查未通过: ${probe.Message}`);
return { Ok: false, Id: 0, Message: probe.Message };
}
log?.(`[probe] 检查通过: Viper ${pe.Version}(${pe.Arch})`);
return {
Ok: true,
Id: 0,
Environment: {
Id: randomUUID(),
Executable: exePath,
Version: pe.Version,
Arch: pe.Arch,
// 与 VET check 命中入库时的 source 取值一致(同一语义:本次答复的来源)
Source: 'check',
DisplayName: `Viper ${pe.Version}`,
},
};
}
+11 -10
View File
@@ -256,7 +256,8 @@ async function CreateProjectCommand(): Promise<void> {
}
/**
* 弹文件选择框挑一个 exe,并经 VET 的 check 通道校验。
* 弹文件选择框挑一个 exe,并做 Viper 编译器校验。
* 校验优先走 VET 的 check 通道;VET 未启用/未运行时由扩展本地模拟(读 PE + `--version`,均带超时)。
* 返回回执;用户取消时返回 undefined(校验失败会在这里给出错误提示)。
*/
async function PickAndCheckExecutable(tool: EnvironmentTool): Promise<ViperCheckResult | undefined> {
@@ -272,12 +273,12 @@ async function PickAndCheckExecutable(tool: EnvironmentTool): Promise<ViperCheck
}
return window.withProgress(
{ location: ProgressLocation.Notification, title: `VET 正在检查 ${path.basename(exePath)}…` },
{ location: ProgressLocation.Notification, title: `正在检查 ${path.basename(exePath)}…` },
async () => {
try {
return await tool.CheckExecutable(exePath);
} catch (e) {
void window.showErrorMessage(`VET 检查失败:${e instanceof Error ? e.message : String(e)}`);
void window.showErrorMessage(`检查失败:${e instanceof Error ? e.message : String(e)}`);
return undefined;
}
}
@@ -285,8 +286,8 @@ async function PickAndCheckExecutable(tool: EnvironmentTool): Promise<ViperCheck
}
/**
* 「选择编译器…」:选一个 exe,经 VET check 通过后并入环境清单并立刻切换过去
* (VET 会把该 exe 入库,后续轮次快照里也会带着它)。
* 「选择编译器…」:选一个 exe,校验通过后并入环境清单并立刻切换过去
* (走 VET check 时 VET 会把该 exe 入库,后续轮次快照里也会带着它)。
*/
async function PickManualCompiler(
tool: EnvironmentTool,
@@ -305,7 +306,7 @@ async function PickManualCompiler(
return selected;
}
void window.showErrorMessage(`VET 检查未通过:${DescribeCheckMessage(result.Message)}`);
void window.showErrorMessage(`检查未通过:${DescribeCheckMessage(result.Message)}`);
return undefined;
}
@@ -320,7 +321,7 @@ async function CheckEnvironmentCommand(tool: EnvironmentTool): Promise<void> {
if (result.Ok && environment) {
void window
.showInformationMessage(
`VET 检查通过:${environment.DisplayName}(${environment.Arch})`,
`检查通过:${environment.DisplayName}(${environment.Arch})`,
'选择为当前环境',
'显示详情'
)
@@ -334,13 +335,13 @@ async function CheckEnvironmentCommand(tool: EnvironmentTool): Promise<void> {
return;
}
void window.showErrorMessage(`VET 检查未通过:${DescribeCheckMessage(result.Message)}`);
void window.showErrorMessage(`检查未通过:${DescribeCheckMessage(result.Message)}`);
}
/** 把 VET 回执里的 check_msg 固定原因串翻译成可读说明。 */
/** 把校验回执里的原因串(VET 的 check_msg / 本地模拟的同名原因串)翻译成可读说明。 */
function DescribeCheckMessage(message: string | undefined): string {
if (!message) {
return '未知原因(VET 未给出 check_msg)';
return '未知原因(未给出原因串)';
}
switch (message) {
case 'NOT_FOUND':
+14 -3
View File
@@ -61,9 +61,7 @@ export class EnvironmentStatusBar implements Disposable {
selected ? `当前选中: ${selected.DisplayName}\n${selected.Executable}` : '当前选中: (无)',
`VET: ${this.tool.GetExecutablePath()}`,
'输出: Vipeye VET 输出通道',
state === 'stopped'
? '点击选择编译器(VET 已关闭,已发现的环境仍可选用;重启请用 Restart VET)'
: '点击选择编译器',
DescribePickHint(state),
].join('\n');
this.item.show();
}
@@ -76,6 +74,19 @@ export class EnvironmentStatusBar implements Disposable {
}
}
/**
* 状态栏 tooltip 里的「点击选择编译器」提示。
* VET 没在跑时该入口照样可用:选择时改由扩展**本地**检查该 exe(读 PE + `--version`,带超时),
* 不再像以前那样直接失败,所以提示里要说明这一点。
*/
function DescribePickHint(state: EnvironmentToolState): string {
if (state === 'running') {
return '点击选择编译器';
}
const suffix = '点击选择编译器(VET 未运行,选择时由扩展本地检查该 exe:读 PE + --version)';
return state === 'stopped' ? `${suffix}\n重启 VET 请用命令面板的「Restart VET」` : suffix;
}
function DescribeState(state: EnvironmentToolState): string {
switch (state) {
case 'running':
+1
View File
@@ -17,6 +17,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 a conversion **within the same inheritance hierarchy** is allowed — the upcast (`Sub* → Node*`; `memhub` / `requests` depend on `MemBuddy* → MemManager*`) as well as the downcast (`AST* → Call*`; `CallGenDispatch.vp:824` passes an `ast.AST*` into a `rawptr[ast.Call]` parameter and Viper pointers carry no static type at runtime, so the reinterpret is routine), while two unrelated structs (`Node* → Other*`) and 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
- **Picking a compiler no longer requires VET to be running.** The manual 「选择编译器…」 entry used to go exclusively through VET's check channel (`{"check": path, "id": N}` written to stdin → single-line JSON reply on stdout), which is tied to the long-running process and its heartbeat watchdog: with VET disabled, manually stopped, failed to start, or with `environmentTool.confirmSeconds` = 0, the pick failed outright ("VET 未在运行…" / "check 通道依附心跳看门狗…"). Whenever VET isn't in the `running` state the extension now falls back to a **TypeScript simulation of the very same check** (`executableProbe.ts`): it reads the PE — DOS `MZ` → `e_lfanew` → `PE\0\0` → optional-header magic → section table → the `.PRVERSN` section, with every little-endian field decoded by shift/OR bit operations exactly like `VPe.ReadU16`/`ReadU32` — for the version string and the arch (COFF `Machine`), then runs `<exe> --version` with stdout+stderr captured and a **5 s timeout** (the same `VERSION_PROBE_TIMEOUT_MS` as `VProc.RunVersionProbe`) and requires the first five characters to be "Viper". Failures use the **same fixed reason strings** as VET (`NOT_FOUND`, `NO_PERMISSION`, `NOT_EXE`, `NO_PRVERSN`, `PRVERSN_ERR`, `PREFIX_ERR`, `TIMEOUT`, `EXE_ERR,0x<HEX>`), so the user-visible messages and the whole picker/report path are shared, and VET keeps handling the check (including its cache write-back) whenever it is running. The local path is read-only, and the status-bar hint now states that the exe is checked by the extension while VET isn't running
- **A bound on a generic class's type parameter** (`novirtual class GSList[T: GSListNode]:`) is now honoured, so the generic method bodies that rely on it check cleanly — `includes/linkedlist.vp`'s `append` used to report "无法为类"GSList[T@GSList]*"的属性"Head"赋值 / "GSListNode*"不可分配给"rawptr[type[T@GSList]]"" and "无法为类"GSListNode*"的属性"Next"赋值 / 属性"Next"未知". Three missing pieces had to be filled in: (1) the `CPtr` dereference (member access, attribute-access override and subscript) now concretizes a **TypeVar pointee to its bound**, otherwise `self.Tail.Next` looked for `Next` on `CPtr[T]` instead of on `GSListNode`; (2) assigning a **generic type-parameter value to `CPtr[T]`** is now allowed, mirroring the existing "class instance → `CPtr[T]`" rule (in Viper a class-typed value *is* a pointer), which covers `self.Head = node` / `self.Tail = node`; (3) when the member type resolved on an unspecialized generic base degrades to `CPtr[type]` (a pointer to a class object, i.e. "target type unknown"), it is treated like `rawptr` / `void*` and allowed. Removing the bound keeps the error (an unbounded `T` has no `Next`), so the bound is what makes the generic body type-safe. TransPyC is unaffected — its parser already accepts and skips `[T: bound]`
- Pointers to the **variants of a `t.REnum`** are now compatible with pointers to the REnum root (and vice versa), so `rawptr[LLVMType.Ptr]` converts to `rawptr[LLVMType]` instead of being reported as "无法将"rawptr[Ptr]"类型的参数分配给函数"build_load"中类型为"rawptr[LLVMType]"的参数"ty"". The variant classes are nested inside the root class's suite (`class LLVMType(t.REnum): class Ptr: ... / class Int: ...`) rather than derived from it, so `isDerivedFrom` could not see the relation — yet TransPyV only emits a bitcast between the two addresses, and `match`-variant narrowing already rewrites `CPtr[LLVMType]` into `CPtr[LLVMType.Ptr]` (see `narrowTypeBasedOnClassPattern`), which is exactly what llvmlite does: `CallGenCall.vp` narrows `lb_inner: rawptr[LLVMType]` through a nested `case llvmlite.LLVMType.Ptr(lb_inner2)` and passes it straight back to `build_load(builder, ty: rawptr[LLVMType], ...)`. The relation is detected by full-name nesting (`'LLVMType.Ptr' ⊂ 'LLVMType'`, the same criterion the narrowing uses) together with the root's MRO containing `REnum`; two different variants (`Ptr* → Int*`) and a mix with an unrelated struct are still reported
- 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