diff --git a/packages/vipeye-internal/src/parser/parser.ts b/packages/vipeye-internal/src/parser/parser.ts index bf2c9252..943f3816 100644 --- a/packages/vipeye-internal/src/parser/parser.ts +++ b/packages/vipeye-internal/src/parser/parser.ts @@ -2111,11 +2111,22 @@ export class Parser { this._isInFinallyBlock = false; this._isInFinallyLoop = false; - const suite = this._parseSuite(/* isFunction */ true, this._parseOptions.skipFunctionAndClassBody, () => { - if (!functionTypeAnnotationToken) { - functionTypeAnnotationToken = this._getTypeAnnotationCommentText(); + let suite: SuiteNode; + const possibleSuiteStart = this._peekToken(); + if (viperModifier === 'extern' && possibleSuiteStart.type !== TokenType.Colon) { + // Viper extern 声明允许省略函数体(`extern def f(...) -> T`,无冒号、无 pass)。 + // 创建空 suite 并消费行尾换行;`: pass` 方式仍走 _parseSuite 正常解析。 + suite = SuiteNode.create(possibleSuiteStart); + if (this._consumeTokenIfType(TokenType.NewLine)) { + extendRange(suite, this._tokenizerOutput!.tokens.getItemAt(this._tokenIndex - 1)); } - }); + } else { + suite = this._parseSuite(/* isFunction */ true, this._parseOptions.skipFunctionAndClassBody, () => { + if (!functionTypeAnnotationToken) { + functionTypeAnnotationToken = this._getTypeAnnotationCommentText(); + } + }); + } this._isInExceptionGroup = wasInExceptionGroup; this._isInFinallyBlock = wasInFinallyBlock; diff --git a/packages/vipeye-internal/src/parser/tokenizer.ts b/packages/vipeye-internal/src/parser/tokenizer.ts index c4c09030..557767c1 100644 --- a/packages/vipeye-internal/src/parser/tokenizer.ts +++ b/packages/vipeye-internal/src/parser/tokenizer.ts @@ -675,18 +675,24 @@ function getViperStatementKeywordAction( return 'viperModifier'; } if (name === 'fn' || name === 'fun' || name === 'func' || name === 'function') { - // def 别名:语句起始(或紧跟在修饰关键字后,如 `inline fn f()`)且后跟 - // 标识符(函数名)时按 def 关键字处理。 + // def 别名:语句起始(或紧跟修饰关键字后,如 `inline fn f()`)时: + // 后跟标识符(函数名)→ def; + // 行尾单独使用(后面无任何内容)→ def(parser 会报缺函数名); + // 后跟 ':' / '=' / '(' 等 → 保持标识符(保护 TransPyC 现有 + // `func:` 注解、`func = x` 赋值、`func(x)` 调用等合法用法)。 if (!isViperAtStatementStart(text, identifierStart) && !precededByViperModifier) { return undefined; } - return nextWord.length > 0 && isIdentifierStartChar(nextWord.charCodeAt(0)) ? 'def' : undefined; + if (nextWord.length > 0) { + return isIdentifierStartChar(nextWord.charCodeAt(0)) ? 'def' : undefined; + } + return peekViperNextNonWsChar(text, identifierEnd) === undefined ? 'def' : undefined; } - if (name === 'enum' || name === 'rnum') { + if (name === 'enum' || name === 'rnum' || name === 'union') { if (nextWord === 'class' || nextWord === 'struct') { return 'skip'; } - // `enum A:` / `rnum A:` 单独使用(等价 class A:)。 + // `enum A:` / `rnum A:` / `union A:` 单独使用(等价 class A:)。 return isViperAtStatementStart(text, identifierStart) && nextWord.length > 0 && isIdentifierStartChar(nextWord.charCodeAt(0)) @@ -734,6 +740,19 @@ function isViperAtStatementStart(text: string, identifierStart: number): boolean return pos === 0 || text[pos - 1] === '\n' || text[pos - 1] === '\r'; } +// 跳过空白后返回首个非空白字符;若到行尾(换行)或文件结束则返回 undefined。 +function peekViperNextNonWsChar(text: string, from: number): string | undefined { + let pos = from; + const len = text.length; + while (pos < len && (text[pos] === ' ' || text[pos] === '\t')) { + pos++; + } + if (pos < len && text[pos] !== '\r' && text[pos] !== '\n') { + return text[pos]; + } + return undefined; +} + export class Tokenizer { private _cs = new CharacterStream(''); private _tokens: Token[] = []; diff --git a/packages/vipeye-internal/src/tests/viperGenericTypeVarCall.test.ts b/packages/vipeye-internal/src/tests/viperGenericTypeVarCall.test.ts index 86906f9e..e3a79038 100644 --- a/packages/vipeye-internal/src/tests/viperGenericTypeVarCall.test.ts +++ b/packages/vipeye-internal/src/tests/viperGenericTypeVarCall.test.ts @@ -364,6 +364,15 @@ rnum B: class Inner: x: t.CInt +# union + class(等价 t.CUnion 风格) +union class Value: + i: t.CInt + f: t.CFloat + +# union 单独使用(等价 class) +union D: + x: t.CInt + def swap(p: IntPair | t.CPtr): tmp: t.CInt = c.Deref(p).a c.Deref(p).a = c.Deref(p).b @@ -372,6 +381,7 @@ def swap(p: IntPair | t.CPtr): def test_usage(): r: t.CInt = double_export(21) g: t.CInt = Color.GREEN + u: t.CFloat = Value.f `; const testDir = path.join(__dirname, '__viper_keywords_test__'); const fs = require('fs'); @@ -518,6 +528,12 @@ extern def e_ok2(x: t.CInt) -> t.CInt: extern def e_bad(x: t.CInt) -> t.CInt: return x +# extern 声明可以省略函数体(无冒号、无 pass),": pass" 方式保留 +extern def e_no_body(x: t.CInt) -> t.CInt + +# extern + fn 别名,同样可以省略函数体 +extern fn e_fn_no_body(x: t.CInt) -> t.CInt + def use(): a: t.CInt = f1(1) + f2(2) + f3(3) + f4(4) `; @@ -556,4 +572,147 @@ def use(): fs.rmSync(testDir, { recursive: true, force: true }); } serviceProvider.dispose(); -}); \ No newline at end of file +}); + +test('Viper fn/fun/func/function 单独使用报错,后跟 :/=/ 保持标识符', () => { + 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 code = `import t + +# fn 行尾单独使用 → 视为 def,报缺函数名 +fn + +# fun 后跟 '=' → 保持标识符(赋值,不是关键字) +fun = 5 + +# func 后跟 '(' → 保持标识符(调用,不是关键字) +func(1) + +# function 后跟 ':' → 保持标识符(注解声明,不是关键字) +function: t.CInt = 0 + +# 正常用法不受影响 +fn ok(x: t.CInt) -> t.CInt: + return x +`; + const testDir = path.join(__dirname, '__viper_fn_keyword_test__'); + const fs = require('fs'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'fn_keyword.vp'); + fs.writeFileSync(testFile, code, 'utf8'); + + try { + const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); + const program = new Program(importResolver, configOptions, serviceProvider); + const uri = UriEx.file(testFile); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== fn_keyword.vp diagnostics ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + // 期望恰好 1 个错误级诊断:fn 行尾单独使用报"缺函数名"; + // fun=/func(/function: 保持标识符不应产生语法/类型错误。 + const errors = diagnostics.filter((d) => d.startsWith('[category=0]')); + assert.strictEqual( + errors.length, + 1, + `expected exactly 1 error (fn alone), got: ${JSON.stringify(diagnostics)}` + ); + assert.ok( + errors[0].includes('函数名') || errors[0].includes('function name'), + `expected missing-function-name error: ${JSON.stringify(errors)}` + ); + + program.dispose(); + } finally { + fs.rmSync(testDir, { recursive: true, force: true }); + } + serviceProvider.dispose(); +}); + +test('builtins.vp 中 Asm/LLVMIR/AsmInp/AsmOut/LInp/LOut 签名可用且不报未定义', () => { + 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 code = `import t + +def test_builtins_asm_llvmir(): + r: t.CInt = 0 + # Asm:基础 + 输出/输入操作数 + 破坏列表 + Asm("nop") + Asm(f"mov {AsmOut(r, t.ASM_DESCR.OUTPUT_REG)}, rdi", op=[t.ASM_DESCR.CLOBBER_MEMORY]) + Asm(f"mov eax, {AsmInp(r, t.ASM_DESCR.REG_ANY)}") + # LLVMIR:LInp/LOut 标记操作数 + LLVMIR(f"{LOut(r)} = add i32 {LInp(r)}, {LInp(r)}") +`; + const testDir = path.join(__dirname, '__viper_builtins_asm_test__'); + const fs = require('fs'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'builtins_asm.vp'); + fs.writeFileSync(testFile, code, 'utf8'); + + try { + const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); + const program = new Program(importResolver, configOptions, serviceProvider); + const uri = UriEx.file(testFile); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== builtins_asm.vp diagnostics ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + // 期望没有任何错误级诊断:Asm/LLVMIR/AsmInp/AsmOut/LInp/LOut 由隐式 + // builtins(builtins.vpi)提供,无需 import 即可直接使用。 + const errors = diagnostics.filter((d) => d.startsWith('[category=0]')); + assert.strictEqual( + errors.length, + 0, + `unexpected error diagnostics: ${JSON.stringify(diagnostics)}` + ); + + program.dispose(); + } finally { + fs.rmSync(testDir, { recursive: true, force: true }); + } + serviceProvider.dispose(); +}); diff --git a/packages/vipeye-internal/typeshed-fallback/stdlib/builtins.vpi b/packages/vipeye-internal/typeshed-fallback/stdlib/builtins.vpi index 12c68f19..7379d31f 100644 --- a/packages/vipeye-internal/typeshed-fallback/stdlib/builtins.vpi +++ b/packages/vipeye-internal/typeshed-fallback/stdlib/builtins.vpi @@ -2560,3 +2560,34 @@ if sys.version_info >= (3, 11): if sys.version_info >= (3, 13): class PythonFinalizationError(RuntimeError): ... + +# --- Viper 扩展:C 内联汇编 / LLVM IR 内联设施 --- +# 签名照抄 vipershed-fallback/stdlib/c.vpi(c.Asm / c.LLVMIR / c.AsmInp / c.AsmOut / +# c.LInp / c.LOut),供 Viper 代码直接以内置函数形式使用。 + +def Asm(asm: str, *args: Any, inp: Any = None, out: Any = None, op: Any = None) -> Any: + """内联汇编(等价 c.Asm)。inp 为输入操作数描述符列表,out 为输出操作数 + 描述符列表,op 为破坏描述符列表。""" + ... + +def LLVMIR(ir: str, *args: Any, inp: Any = None, out: Any = None, op: Any = None) -> Any: + """LLVM IR 内联(等价 c.LLVMIR)。inp/out/op 为操作数描述符列表。""" + ... + +def AsmInp(expr: Any, constraint: Any = None) -> Any: + """汇编输入操作数标记(等价 c.AsmInp)。constraint 为寄存器约束 + (如 t.ASM_DESCR.REG_ANY)。""" + ... + +def AsmOut(expr: Any, constraint: Any = None) -> Any: + """汇编输出操作数标记(等价 c.AsmOut)。constraint 为寄存器约束 + (如 t.ASM_DESCR.OUTPUT_REG)。""" + ... + +def LInp(expr: Any, constraint: Any = None) -> Any: + """LLVM IR 输入操作数标记(等价 c.LInp)。""" + ... + +def LOut(expr: Any, constraint: Any = None) -> Any: + """LLVM IR 输出操作数标记(等价 c.LOut)。""" + ... \ No newline at end of file diff --git a/packages/vscode-vipeye/language-configuration.json b/packages/vscode-vipeye/language-configuration.json index 7e67bec8..264029e1 100644 --- a/packages/vscode-vipeye/language-configuration.json +++ b/packages/vscode-vipeye/language-configuration.json @@ -28,7 +28,7 @@ ], "onEnterRules": [ { - "beforeText": "^\\s*(?:(?:inline|export|extern|enum|rnum)\\s+)?(?:def|fn|fun|func|function|class|for|if|elif|else|while|try|finally|with|except|async|struct)\\b.*?:\\s*$", + "beforeText": "^\\s*(?:(?:inline|export|extern|enum|rnum|union)\\s+)?(?:def|fn|fun|func|function|class|for|if|elif|else|while|try|finally|with|except|async|struct)\\b.*?:\\s*$", "action": { "indent": "indent" } }, { @@ -45,7 +45,7 @@ }, "wordPattern": "(?x)\n \\b\\w*\\b \n | (?<=\\.)\\w+\n | (?<=@)\\w+\n | \\p{Lu}[\\p{Ll}_\\p{Lu}\\p{Nd}]*\n | [[:alpha:]_][[:alpha:]_\\p{Nd}]*\n | \\p{Lu}[\\p{Ll}_\\p{Lu}\\p{Nd}]*f?\\b", "indentationRules": { - "increaseIndentPattern": "^\\s*(?:(?:inline|export|extern|enum|rnum)\\s+)?(?:class|def|fn|fun|func|function|elif|else|except|finally|for|if|try|while|with|struct|async\\s+def|async\\s+for|async\\s+with)\\b.*?:\\s*$", + "increaseIndentPattern": "^\\s*(?:(?:inline|export|extern|enum|rnum|union)\\s+)?(?:class|def|fn|fun|func|function|elif|else|except|finally|for|if|try|while|with|struct|async\\s+def|async\\s+for|async\\s+with)\\b.*?:\\s*$", "decreaseIndentPattern": "^\\s*(elif|else|except|finally)\\b.*?:\\s*$" } } diff --git a/packages/vscode-vipeye/syntaxes/viper.tmLanguage.json b/packages/vscode-vipeye/syntaxes/viper.tmLanguage.json index 37994255..2fb9939e 100644 --- a/packages/vscode-vipeye/syntaxes/viper.tmLanguage.json +++ b/packages/vscode-vipeye/syntaxes/viper.tmLanguage.json @@ -39,7 +39,7 @@ }, { "name": "storage.type.function.viper", - "match": "\\b(fn|fun|func|function)\\b(?=\\s+[A-Za-z_][A-Za-z0-9_]*)" + "match": "\\b(fn|fun|func|function)\\b(?=\\s+[A-Za-z_][A-Za-z0-9_]*|\\s*$)" }, { "name": "storage.type.class.viper", @@ -47,11 +47,11 @@ }, { "name": "storage.modifier.viper", - "match": "\\b(inline|export|extern)\\b(?=\\s+def\\b|[ \\t]*$)" + "match": "\\b(inline|export|extern)\\b(?=\\s+(?:def|fn|fun|func|function)\\b|[ \\t]*$)" }, { "name": "storage.type.class.viper", - "match": "\\b(enum|rnum)\\b(?=\\s+(class|struct)\\b|\\s+[A-Za-z_][A-Za-z0-9_]*)" + "match": "\\b(enum|rnum|union)\\b(?=\\s+(class|struct)\\b|\\s+[A-Za-z_][A-Za-z0-9_]*)" }, { "name": "storage.type.class.viper",