feat(vipeye-internal): 支持 match-case 内裸 break 与 fallthrough
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled

- break:parser 新增 _isInMatchCase(解析 case body 期间置位,与 _isInLoop 同法),_parseBreakStatement 判据放宽为 !_isInLoop && !_isInMatchCase,不再误报 breakOutsideLoop

- fallthrough:新增 ParseNodeType.Fallthrough / FallthroughNode;作为软关键字仅在「viper 方言 + case 体内 + 独占简单语句」时成立,其它位置仍是普通标识符

- 同步 parseTreeWalker 两处 switch 与 visitor、parseNodeUtils 的 ParseNodeTypeMap、binder 流程终止、flowChartBuilder 终止语句、completion 关键字、TextMate 高亮

- 测试 viperMatchBreakFallthrough.test.ts(2 用例),全量 jest 825 -> 827 通过
This commit is contained in:
TermiNexus committed 2026-10-02 22:29:53 +08:00
1 parent 9e0d12553d
commit 5463fac3d2
11 files changed
+300 -4

No files matched your search

+2
View File
@@ -169,6 +169,8 @@ packages/vipeye-internal/src/tests/__viper_renum_pointer__/
packages/vipeye-internal/src/tests/__viper_renum_pointer_matrix__/
packages/vipeye-internal/src/tests/__viper_generic_bound__/
packages/vipeye-internal/src/tests/__viper_generic_bound_none__/
packages/vipeye-internal/src/tests/__viper_match_break_fallthrough__/
packages/vipeye-internal/src/tests/__viper_match_break_fallthrough_neg__/
# perfCompare.py cached commit bundles
/build/perfCompare/
@@ -44,6 +44,7 @@ import {
DelNode,
ExceptNode,
ExpressionNode,
FallthroughNode,
ForNode,
FunctionNode,
GlobalNode,
@@ -1448,6 +1449,17 @@ export class Binder extends ParseTreeWalker {
return false;
}
override visitFallthrough(node: FallthroughNode): boolean {
// Viper 扩展:`fallthrough` 结束当前 case 体(控制流交给下一个 case),
// 故本条之后同 case 体内的语句不可达。去向不是「match 之后」,
// 故不像 break 那样挂到 _currentBreakTarget;下一个 case 的入口不建模
// (visitMatch 的 postMatchLabel 依赖「未终止的 case 体」收口)。
this._currentFlowNode = Binder._unreachableStructuralFlowNode;
// Fallthrough nodes don't have any children.
return false;
}
override visitReturn(node: ReturnNode): boolean {
if (this._targetFunctionDeclaration) {
if (!this._targetFunctionDeclaration.returnStatements) {
@@ -302,7 +302,10 @@ class FlowChartBuilder {
}
}
/** 是否为终止语句(return / raise / break / continue)。它们位于 StatementList 内部。 */
/**
* 是否为终止语句(return / raise / break / continue / fallthrough)。
* 它们位于 StatementList 内部。
*/
private _isTerminatorStatement(statement: StatementNode): boolean {
if (statement.nodeType !== ParseNodeType.StatementList) {
return false;
@@ -316,7 +319,9 @@ class FlowChartBuilder {
nodeType === ParseNodeType.Return ||
nodeType === ParseNodeType.Raise ||
nodeType === ParseNodeType.Break ||
nodeType === ParseNodeType.Continue
nodeType === ParseNodeType.Continue ||
// Viper 扩展:match-case 内的 `fallthrough`(case 穿透)同样终止当前分支。
nodeType === ParseNodeType.Fallthrough
);
}
@@ -33,6 +33,7 @@ import {
EllipsisNode,
ErrorNode,
ExceptNode,
FallthroughNode,
ForNode,
FormatStringNode,
FunctionAnnotationNode,
@@ -185,6 +186,9 @@ export function getChildNodes(node: ParseNode): (ParseNode | undefined)[] {
case ParseNodeType.Except:
return [node.d.typeExpr, node.d.name, node.d.exceptSuite];
case ParseNodeType.Fallthrough:
return [];
case ParseNodeType.For:
return [node.d.targetExpr, node.d.iterableExpr, node.d.forSuite, node.d.elseSuite];
@@ -441,6 +445,9 @@ export class ParseTreeVisitor<T> {
case ParseNodeType.Except:
return this.visitExcept(node);
case ParseNodeType.Fallthrough:
return this.visitFallthrough(node);
case ParseNodeType.For:
return this.visitFor(node);
@@ -711,6 +718,11 @@ export class ParseTreeVisitor<T> {
return this._default;
}
// Viper 扩展:match-case 内的裸 `fallthrough` 语句(case 穿透)。
visitFallthrough(node: FallthroughNode) {
return this._default;
}
visitFor(node: ForNode) {
return this._default;
}
@@ -205,6 +205,7 @@ namespace Keywords {
'const',
'volatile',
'typedef',
'fallthrough',
'inline',
'export',
'extern',
@@ -37,6 +37,7 @@ export const ParseNodeTypeMap: ParseNodeTypeMapType = {
DictionaryExpandEntry: ParseNodeType.DictionaryExpandEntry,
DictionaryKeyEntry: ParseNodeType.DictionaryKeyEntry,
Ellipsis: ParseNodeType.Ellipsis,
Fallthrough: ParseNodeType.Fallthrough,
If: ParseNodeType.If,
Import: ParseNodeType.Import,
ImportAs: ParseNodeType.ImportAs,
@@ -49,6 +49,7 @@ export const enum ParseNodeType {
DictionaryKeyEntry, // 20
Ellipsis,
Fallthrough,
If,
Import,
ImportAs,
@@ -2395,6 +2396,30 @@ export namespace ContinueNode {
}
}
/**
* Viper 扩展:`fallthrough` 语句(case 穿透)。
* 与 `break` 同属「终止当前 match 分支」的控制流语句,区别是去向为**下一个 case 体**
* 而不是 match 之后。是软关键字:仅在 match-case 体内、且独占一条简单语句时才是本节点,
* 其它位置仍是普通标识符(见 parser._isAtViperFallthroughStatement)。
*/
export interface FallthroughNode extends ParseNodeBase<ParseNodeType.Fallthrough> {}
export namespace FallthroughNode {
export function create(range: TextRange) {
const node: FallthroughNode = {
start: range.start,
length: range.length,
nodeType: ParseNodeType.Fallthrough,
id: _nextNodeId++,
parent: undefined,
a: undefined,
d: {},
};
return node;
}
}
export interface ReturnNode extends ParseNodeBase<ParseNodeType.Return> {
d: {
expr?: ExpressionNode | undefined;
@@ -2873,6 +2898,7 @@ export type ParseNode =
| DictionaryExpandEntryNode
| DictionaryKeyEntryNode
| EllipsisNode
| FallthroughNode
| IfNode
| ImportNode
| ImportAsNode
+45 -1
View File
@@ -68,6 +68,7 @@ import {
ErrorNode,
ExceptNode,
ExpressionNode,
FallthroughNode,
ForNode,
FormatStringNode,
FunctionAnnotationNode,
@@ -244,6 +245,10 @@ export class Parser {
private _parseOptions: ParseOptions = new ParseOptions();
private _diagSink: DiagnosticSink = new DiagnosticSink();
private _isInLoop = false;
// Viper 扩展:正在解析 match 的某个 case 体。与 _isInLoop 一样是"可 break 作用域"
// 的一种:match 内的裸 break 表示结束整个 match(对齐编译器语义),故不再报
// breakOutsideLoop;fallthrough 也仅在此作用域内才是软关键字语句。
private _isInMatchCase = false;
private _isInFunction = false;
private _isInExceptionGroup = false;
private _isParsingTypeAnnotation = false;
@@ -988,7 +993,13 @@ export class Parser {
guardExpression = this._parseTestExpression(/* allowAssignmentExpression */ true);
}
// Viper 扩展:case 体是「可 break 作用域」——裸 break 结束整个 match,
// fallthrough 收窄为软关键字语句;离开 case 体后恢复(与循环的 _isInLoop 同法)。
const wasInMatchCase = this._isInMatchCase;
this._isInMatchCase = true;
const suite = this._parseSuite(this._isInFunction);
this._isInMatchCase = wasInMatchCase;
return CaseNode.create(caseToken, casePattern, this._isPatternIrrefutable(casePattern), guardExpression, suite);
}
@@ -2933,7 +2944,9 @@ export class Parser {
private _parseBreakStatement(): BreakNode {
const breakToken = this._getKeywordToken(KeywordType.Break);
if (!this._isInLoop) {
// Viper 扩展:match-case 内的裸 break 表示结束整个 match(对齐编译器语义),
// 与循环内的 break 一样属于合法「可跳出作用域」,不再报 breakOutsideLoop。
if (!this._isInLoop && !this._isInMatchCase) {
this._addSyntaxError(LocMessage.breakOutsideLoop(), breakToken);
} else if (this._isInExceptionGroup) {
this._addSyntaxError(LocMessage.breakInExceptionGroup(), breakToken);
@@ -3460,6 +3473,11 @@ export class Parser {
return this._parseViperTypedefStatement();
}
// Viper 扩展:match-case 内的裸 `fallthrough`(case 穿透)软关键字语句。
if (this._isAtViperFallthroughStatement()) {
return this._parseViperFallthroughStatement();
}
switch (this._peekKeywordType()) {
case KeywordType.Pass:
return this._parsePassStatement();
@@ -3611,6 +3629,32 @@ export class Parser {
return AssignmentNode.create(leftExpr, rightExpr);
}
// Viper 扩展:判定语句起始处是否为 match-case 体内的裸 `fallthrough`。
// `fallthrough` 是软关键字,三个条件同时满足才算语句:
// ① 方言为 viper;② 正处于某个 case 体内(_isInMatchCase);
// ③ 冒号后独占一条简单语句 —— 后跟行尾 / `;` / 文件尾。
// 不满足时保持普通标识符解析(`fallthrough = 1` 是赋值、`fallthrough()` 是调用、
// `x.fallthrough` 是成员访问;case 外的裸 `fallthrough` 仍按未定义名报错)。
private _isAtViperFallthroughStatement(): boolean {
if (this._parseOptions.dialect !== 'viper' || !this._isInMatchCase) {
return false;
}
const keywordToken = this._peekToken();
if (keywordToken.type !== TokenType.Identifier || (keywordToken as IdentifierToken).value !== 'fallthrough') {
return false;
}
const nextType = this._peekToken(1).type;
return nextType === TokenType.NewLine || nextType === TokenType.Semicolon || nextType === TokenType.EndOfStream;
}
// Viper 扩展:`fallthrough` 语句(case 穿透)。节点只承载位置,
// 「穿透到下一个 case」的语义由编译器(TransPyC/TransPyV)承担。
private _parseViperFallthroughStatement(): FallthroughNode {
const fallthroughToken = this._getNextToken();
assert(fallthroughToken.type === TokenType.Identifier);
return FallthroughNode.create(fallthroughToken);
}
private _makeExpressionOrTuple(
exprListResult: ListResult<ExpressionNode>,
enclosedInParens: boolean
@@ -0,0 +1,192 @@
/*
* viperMatchBreakFallthrough.test.ts
*
* 验证 match-case 内支持**裸 `break`** 与**裸 `fallthrough`**:
* - `break` → 结束整个 match(对齐编译器语义,不再报 breakOutsideLoop);
* - `fallthrough` → case 穿透(软关键字语句)。
*
* 背景:TPC/TransPyV 正在支持这两个裸关键字(此前只有 `c.Break()` / `c.NoBreak()`),
* LSP 侧原先会把 match 内的裸 break 报成 breakOutsideLoop、把裸 fallthrough 当成未定义标识符。
*
* 实现要点(仅 LSP):
* ① parser 新增 `_isInMatchCase`,在 `_parseCaseStatement` 解析 case body 期间置位
* (与循环的 `_isInLoop` 同法保存/恢复);
* ② `_parseBreakStatement` 判据放宽为 `!_isInLoop && !_isInMatchCase` 才报错;
* ③ 新增 `ParseNodeType.Fallthrough` / `FallthroughNode`,parser 里以**软关键字**识别:
* 仅当①方言为 viper、②处于 case 体内、③后跟行尾/`;`/文件尾 时才是该语句,
* 其它位置仍是普通标识符(`fallthrough = 1` / `fallthrough()` / `x.fallthrough`)。
* ④ binder 把 fallthrough 当作流程终止(其后同 case 体语句不可达);
* flowChartBuilder 把它并入终止语句。
*
* 运行:npx jest src/tests/viperMatchBreakFallthrough.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()) {
// 循环
}
return { program, configOptions, uri, serviceProvider, testDir };
}
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,
}));
}
/** 实际报错(category 0=Error / 1=Warning)所在行集合。 */
function hardErrorLines(program: Program, configOptions: ConfigOptions, uri: any): number[] {
return Array.from(
new Set(
getDiagnostics(program, configOptions, uri)
.filter((d) => d.category === 0 || d.category === 1)
.map((d) => d.line)
)
).sort((a, b) => a - b);
}
/** match-case 内裸 break(结束 match)+ 裸 fallthrough(穿透到下一个 case)。 */
const MATCH_CODE = `import t
from stdint import *
def pick(x: int) -> int:
total: int = 0
match x:
case 0:
total = 1
break
case 1:
total = 2
fallthrough
case 2:
total = 3
break
case _:
total = 4
return total
`;
/** 反例:case 外的裸 break / 裸 fallthrough 仍应按原有规则报错。 */
const NEGATIVE_CODE = `import t
from stdint import *
def f() -> int:
break
return 0
def g() -> int:
fallthrough
return 0
`;
test('match-case 内裸 break 与 fallthrough 均不报错', () => {
const { program, configOptions, uri, testDir, serviceProvider } = buildProgramForCode(
MATCH_CODE,
'__viper_match_break_fallthrough__'
);
try {
const diagnostics = getDiagnostics(program, configOptions, uri);
console.log('=== match break/fallthrough 诊断 ===');
diagnostics
.filter((d) => d.category === 0 || d.category === 1)
.forEach((d) => console.log(` L${d.line} [cat=${d.category}] ${d.message}`));
const lines = hardErrorLines(program, configOptions, uri);
console.log('=== 报错行 ===');
lines.forEach((l) => console.log(` L${l}`));
assert.deepStrictEqual(
lines,
[],
`case 体内的 break(结束 match)与 fallthrough(case 穿透)都不应报错:${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('case 外的裸 break 报 breakOutsideLoop、裸 fallthrough 仍是未定义标识符', () => {
const { program, configOptions, uri, testDir, serviceProvider } = buildProgramForCode(
NEGATIVE_CODE,
'__viper_match_break_fallthrough_neg__'
);
try {
const diagnostics = getDiagnostics(program, configOptions, uri);
console.log('=== case 外 break/fallthrough 诊断 ===');
diagnostics
.filter((d) => d.category === 0 || d.category === 1)
.forEach((d) => console.log(` L${d.line} [cat=${d.category}] ${d.message}`));
// L5:循环/match 之外的 break 仍报 breakOutsideLoop;
// L9:case 之外的 fallthrough 不是软关键字语句,按未定义标识符报错。
const lines = hardErrorLines(program, configOptions, uri);
console.log('=== 报错行 ===');
lines.forEach((l) => console.log(` L${l}`));
assert.deepStrictEqual(
lines,
[5, 9],
`case 外的 break(L5)与 fallthrough(L9)都应报错:${JSON.stringify(
diagnostics.filter((d) => d.category === 0 || d.category === 1)
)}`
);
} finally {
program.dispose();
serviceProvider.dispose();
require('fs').rmSync(testDir, { recursive: true, force: true });
}
});
+1
View File
@@ -2,6 +2,7 @@
## 1.1.427
- **Bare `break` and `fallthrough` inside a `match` statement are now accepted** (LSP side only; TransPyC/TransPyV are being taught the same syntax separately). A `break` in a `case` body ends the whole match, so it is no longer reported as `breakOutsideLoop`: the parser tracks a second "breakable scope", `_isInMatchCase`, set while a `case` suite is parsed and restored afterwards, exactly like the existing loop flag (`continue` is untouched). `fallthrough` (case fall-through) becomes a real statement node (`ParseNodeType.Fallthrough` / `FallthroughNode`) but stays a **soft keyword** — it is that statement only when the dialect is Viper, the parser is inside a case body, and it forms a complete simple statement (followed by a newline, `;` or end of file). Everywhere else — `fallthrough = 1`, `fallthrough()`, `x.fallthrough`, or a bare `fallthrough` outside a `match` — it remains an ordinary identifier, so a stray one outside a case is still reported as undefined. The binder treats the node as a flow terminator (statements after it in the same case are unreachable), the flow chart lists it among the terminator statements, completion offers it, and the TextMate grammar highlights it as `keyword.control.flow.viper`
- **Auto-discovered compiler `includes`**: when a project does not declare `includes` in `project.vpj` / `project.json`, the extension now falls back to locating the compiler's own standard-library includes automatically. The baseline is the **directory of the currently selected compiler executable** (not the `.vp` file being analyzed): the client walks up at most 4 levels from the executable's directory and, at each level, looks only at the *immediate* subdirectories `includes` / `include` (no recursion), keeping the ones that actually exist. The result is appended to `viper.analysis.extraPaths` in the existing `workspace/configuration` middleware, so the server adds them to the import search roots *after* the project-declared `includes` — e.g. a `TransPyV.exe` under `TransPyC/exe` makes `TransPyC/includes` importable, mirroring `project.vpj`'s `"includes": ["../includes"]`. Non-Viper toolchains are unaffected (a nonexistent executable directory yields no candidates). Switching compilers re-sends `workspace/didChangeConfiguration`, so the new roots take effect without reloading the window
- `__attribute__` no longer requires the file to `import t` / `import c`: the `t` / `c` module roots that `__attribute__(...)` desugars into are synthesized as **implicit module names** (zero-length, marked on the AST node — the compiler's attribute handling is AST-level and never checks symbol visibility either). When such a synthetic name cannot be resolved through the lexical scope chain, it falls back to the same-named binding in the built-in `builtins.vpi` stub; the fallback only applies to the desugared nodes, so a hand-written bare `t` / `c` in a file without the import still reports "未定义"
- **Analysis progress in the status bar**: while the language server builds the dependency graph and type-checks the workspace, the bottom-left status bar now shows `$(sync~spin) C:\xxx\1.vp (4/90) 4.44% loading...` — the file currently being analyzed, how many files have been processed out of how many, and the percentage. The worker reports per-file progress (throttled to one message every 50 ms) through a new `analysisProgress` background message and a new `viper/analysisStatus` notification; the client shows a left-aligned `StatusBarItem` and hides it as soon as analysis finishes (or when there is nothing to analyze). Previously the only feedback was the work-done progress bar for open-files analysis, which said nothing about which file was being processed, so a long full-workspace pass was indistinguishable from a hung server
@@ -31,7 +31,7 @@
"patterns": [
{
"name": "keyword.control.flow.viper",
"match": "\\b(if|elif|else|while|for|in|return|break|continue|pass|raise|try|except|finally|with|as|import|from|yield|async|await|del|assert|lambda|match|case|define)\\b"
"match": "\\b(if|elif|else|while|for|in|return|break|continue|fallthrough|pass|raise|try|except|finally|with|as|import|from|yield|async|await|del|assert|lambda|match|case|define)\\b"
},
{
"name": "storage.type.function.viper",