diff --git a/docs/PY2PHP.md b/docs/PY2PHP.md index db24dbbe..8f614eb8 100644 --- a/docs/PY2PHP.md +++ b/docs/PY2PHP.md @@ -26,21 +26,21 @@ | Python 语法 | 状态 | 转换规则 / 报错 | |---|---|---| | `x = expr` | ✅ | `$x = expr;`,模块级变量自动注入 `global` | -| `x += expr` 等增强赋值 | ✅ | 支持 `+ - * / % ** << >> | ^ &` 系列;`//=` 不支持 | +| `x = y = 1`(链式赋值) | ✅ | `$x = $y = 1;`(仅限名称目标;含属性/下标目标时报错) | +| `x += expr` 等增强赋值 | ✅ | 支持 `+ - * / % ** << >> \| ^ &` 系列;`//=` `@=` 展开为 `python\operator\floordiv/matmul($x, ...)` 调用 | | `x: int = expr` | ✅ | 忽略注解,转换为普通赋值 | -| `x: int`(纯注解) | ❌ | `AnnAssign: annotation-only assignments have no TypePHP runtime value` | -| `x = y = 1` | ❌ | `Assign: chained assignments are not supported yet` | -| `a, b = x`(解构) | ❌ | `Assign: destructuring assignments are not supported yet` | -| `def f(...)` | ✅ | 见「函数签名」;函数名为 `main` 报错(与 TypePHP 入口冲突) | +| `x: int`(纯注解) | ✅ | 转为注释 `// annotation-only declaration: x`,不登记为模块全局 | +| `a, b = x`(解构) | ✅ | `[$a, $b] = $x->toArray();`(PyObject 转 PHP 数组后解构;元素允许名称/属性/下标。嵌套解构、星号解构 `a, *b = x`、链式解构不支持。元素个数不匹配时按 PHP 语义补 null,不报 Python 的 ValueError) | +| `def f(...)` | ✅ | 见「函数签名」;名为 `main` 的函数重命名为 `main_`(避免与 TypePHP 入口冲突),调用点同步改写 | | 嵌套 `def` | ❌ | `FunctionDef: nested functions require Python closure scope analysis` | -| `@decorator` | ❌ | `FunctionDef: function decorators are not supported yet` | +| `@decorator` | ✅ | 见「函数装饰器」 | | `return [expr]` | ✅ | `return [expr];` | | `if / elif / else` | ✅ | 同构转换 | | `while` | ✅ | 同构转换;`while/else` 不支持 | | `for i in iter` | ✅ | `foreach (iter as $i)`;`for/else`、元组目标不支持 | | `break` / `continue` / `pass` | ✅ | `pass` → `// pass` 注释 | | `global x` | ✅ | `global $x;`(与自动注入的 global 并存时会重复出现,冗余但合法,属已知行为) | -| `del x` / `del o.a` / `del d[k]` | ✅ | `unset(...)`;其他目标类型报错 `Delete: unsupported del target` | +| `del x` / `del o.a` / `del d[k]` | ✅ | `unset(...)`;`del (a, b)` 元组/列表目标逐项展开;非法 del 目标(如 `del f()`)由 Python 解析器先行拒绝 | | 模块级字符串字面量(docstring) | ✅ | 转为 `/** ... */` 注释(`*/` 转义为 `* /`) | | `import a.b` | ✅ | `use python\a;`(仅首段作为别名,见「已知行为」) | | `import a.b as x` | ✅ | `use python\a\b as x;`(别名等于末段时省略 `as`) | @@ -90,10 +90,36 @@ | 下标 `a[i]` / 切片 `a[l:u:s]` | ✅ | `$a[$i]` / `$a[python\slice(l, u, s)]`(缺省为 `null`) | | f-string | ✅ | 拼接 + `->toString()`;运算符等优先级敏感表达式整体加括号 | | f-string 的 `!r` 转换 / `:03d` 格式说明 | ❌ | `FormattedValue: formatted f-string conversions are not supported yet` | -| 海象 `:=` | ❌ | `NamedExpr` | +| 海象 `:=` | ✅ | 表达式内赋值 `($n = 10)` | | 推导式 / 生成器表达式 | ❌ | `ListComp` / `SetComp` / `DictComp` / `GeneratorExp` | | `yield` / `yield from` | ❌ | `Yield` / `YieldFrom` | +## 函数装饰器 + +装饰器在 `main()` 起始处(其他顶层语句之前)按 Python 语义**自底向上**重绑定到同名模块变量: + +```python +@a +@b +def greet(): ... +``` + +```php +function greet() { ... } + +function main(): void +{ + global $greet; + $greet = b('greet'); + $greet = a('greet'); + ... +} +``` + +- 装饰器可以是已定义函数、`from m import f` 导入符号、模块属性或装饰器工厂(`@dec('x')` → `$greet = dec('x')('greet');`) +- 被装饰函数名登记为模块全局,所有调用点(包括其他函数体内)经 `global` + 变量间接调用装饰结果:`$greet()` +- 被装饰函数体内的递归调用同样解析到装饰后的变量,与 Python 语义一致 + ## print / sys.exit 降级规则 仅当 PHP 行为与 Python 完全一致时才降级为原生语句: @@ -113,6 +139,8 @@ 2. 函数内显式 `global x` 与按模块全局自动注入的 `global x` 会重复出现(合法 PHP)。 3. `print = str` 这类把内置名赋给变量的写法,右侧按变量处理(`$print = $str;`),不做内置名解析。 4. bytes/complex 字面量的报错没有行号(常量在 AST 加载阶段编码,位置信息未传递)。 +5. 装饰器重绑定统一在 `main()` 起始处执行,与 Python "def 处即装饰" 的精确位置略有差异;装饰器表达式若依赖顶层语句后段的赋值,求值时机可能不同。 +6. 被装饰函数名会登记为模块全局,导致所有函数的自动 `global` 注入清单中出现该名字(冗余但合法)。 ## 运行测试 diff --git a/phpunit/src/PythonTools/PythonToTypePhpConverterTest.php b/phpunit/src/PythonTools/PythonToTypePhpConverterTest.php index 4af0fdf8..1f7f3618 100644 --- a/phpunit/src/PythonTools/PythonToTypePhpConverterTest.php +++ b/phpunit/src/PythonTools/PythonToTypePhpConverterTest.php @@ -240,6 +240,43 @@ PYTHON, 'native-statements.py'); "import os.path as ospath\n", ['use python\\os\\path as ospath;'], ], + '链式赋值' => [ + "x = y = 1\n", + ['global $x, $y;', '$x = $y = 1;'], + ], + '解构赋值转 toArray' => [ + "a, b = x\n", + ['global $a, $b;', '[$a, $b] = $x->toArray();'], + ], + '解构元组字面量' => [ + "a, b = (1, 2)\n", + ['[$a, $b] = python\\tuple([1, 2])->toArray();'], + ], + '解构到属性与下标' => [ + "o.a, d['k'] = pair\n", + ["[\$o->a, \$d['k']] = \$pair->toArray();"], + ], + '整除增强赋值展开为函数调用' => [ + "x = 7\nx //= 2\n", + ['$x = python\\operator\\floordiv($x, 2);'], + ], + '矩阵乘增强赋值展开为函数调用' => [ + "x = a\nx @= b\n", + ['$x = python\\operator\\matmul($x, $b);'], + ], + 'del 元组目标逐项展开' => [ + "x = 1\ny = 2\ndel (x, y)\n", + ['unset($x);', 'unset($y);'], + ], + '纯注解声明转为注释且不登记全局' => [ + "x: int\ny = 1\n", + ['// annotation-only declaration: x', 'global $y;'], + ['global $x'], + ], + 'main 函数重命名为 main_' => [ + "def main():\n return 1\n\nprint(main())\n", + ['function main_()', 'python\\print(main_());'], + ], ]; } @@ -319,9 +356,101 @@ PYTHON, 'native-statements.py'); 'f-string 名称插值' => ['x = 1' . "\n" . 'print(f"{x}")' . "\n", ['echo $x->toString(), "\\n";']], 'f-string 文本与插值拼接' => ['x = 1' . "\n" . 'print(f"v={x}")' . "\n", ["echo 'v=' . \$x->toString(), \"\\n\";"]], 'f-string 运算符整体加括号' => ['x = 1' . "\n" . 'print(f"{x + 1}")' . "\n", ['echo ($x + 1)->toString(), "\\n";']], + '海象运算符' => ["if (n := 10):\n print(n)\n", ['if (($n = 10))', 'python\\print($n);']], ]; } + // --------------------------------------------------------------- + // 函数装饰器 + // --------------------------------------------------------------- + + public function testSimpleDecoratorRebindsModuleVariable(): void + { + $php = $this->convert(<<<'PYTHON' +def dec(f): + return f + +@dec +def greet(): + return 1 + +greet() +PYTHON); + + self::assertStringContainsString('$greet = dec(\'greet\');', $php); + // 调用点经变量间接调用装饰结果,而不是直连原函数 + self::assertStringContainsString('$greet();', $php); + self::assertStringNotContainsString(' greet();', $php); + } + + public function testDecoratorFactoryEvaluatesBeforeRebinding(): void + { + $php = $this->convert(<<<'PYTHON' +def dec(prefix): + return lambda f: f + +@dec('x') +def greet(): + return 1 +PYTHON); + + self::assertStringContainsString('$greet = dec(\'x\')(\'greet\');', $php); + } + + public function testStackedDecoratorsApplyBottomUp(): void + { + $php = $this->convert(<<<'PYTHON' +def a(f): + return f + +def b(f): + return f + +@a +@b +def greet(): + return 1 +PYTHON); + + self::assertStringContainsString('$greet = b(\'greet\');', $php); + self::assertStringContainsString('$greet = a(\'greet\');', $php); + self::assertLessThan( + strpos($php, '$greet = a(\'greet\');'), + strpos($php, '$greet = b(\'greet\');'), + ); + } + + public function testImportedSymbolDecorator(): void + { + $php = $this->convert(<<<'PYTHON' +from functools import cache + +@cache +def compute(): + return 1 +PYTHON); + + self::assertStringContainsString('$compute = python\\functools\\cache(\'compute\');', $php); + } + + /** 被装饰函数进入模块全局,函数内调用点经 global + 变量间接调用。 */ + public function testDecoratedFunctionCallInsideAnotherFunction(): void + { + $php = $this->convert(<<<'PYTHON' +def dec(f): + return f + +@dec +def greet(): + return 1 + +def run(): + return greet() +PYTHON); + + self::assertStringContainsString("function run()\n{\n global \$greet;\n return \$greet();", $php); + } + // --------------------------------------------------------------- // 运算符映射 // --------------------------------------------------------------- @@ -464,24 +593,20 @@ PYTHON, 'native-statements.py'); 'yield' => ["def f():\n yield 1\n", 'case.py:2: unsupported Python syntax Yield'], 'nonlocal' => ["def f():\n x = 1\n nonlocal x\n", 'case.py:3: unsupported Python syntax Nonlocal'], 'and/or 布尔运算' => ["x = a and b\n", 'case.py:1: unsupported Python syntax BoolOp'], - '海象运算符' => ["if (n := 10):\n pass\n", 'case.py:1: unsupported Python syntax NamedExpr'], '列表推导式' => ["x = [i for i in range(3)]\n", 'case.py:1: unsupported Python syntax ListComp'], '字典推导式' => ["x = {k: v for k, v in d}\n", 'case.py:1: unsupported Python syntax DictComp'], '生成器表达式' => ["x = sum(i for i in range(3))\n", 'case.py:1: unsupported Python syntax GeneratorExp'], - '链式赋值' => ["x = y = 1\n", 'case.py:1: unsupported Python syntax Assign: chained assignments'], - '解构赋值' => ["a, b = x\n", 'case.py:1: unsupported Python syntax Assign: destructuring assignments'], - '纯注解赋值' => ["x: int\n", 'case.py:1: unsupported Python syntax AnnAssign: annotation-only'], - '整除增强赋值' => ["x = 1\nx //= 2\n", 'case.py:2: unsupported Python syntax AugAssign: unsupported binary operator FloorDiv'], + '非名称目标的链式赋值' => ["a.b = c = 1\n", 'case.py:1: unsupported Python syntax Assign: chained assignments are only supported for plain name targets'], + '嵌套解构' => ["a, (b, c) = x\n", 'case.py:1: unsupported Python syntax Assign: nested destructuring'], + '星号解构' => ["a, *b = x\n", 'case.py:1: unsupported Python syntax Assign: starred destructuring'], + '链式解构' => ["a, b = c = x\n", 'case.py:1: unsupported Python syntax Assign: destructuring with chained targets'], 'while/else' => ["while True:\n pass\nelse:\n pass\n", 'case.py:1: unsupported Python syntax While: while/else'], 'for/else' => ["for i in x:\n pass\nelse:\n pass\n", 'case.py:1: unsupported Python syntax For: for/else'], 'for 元组目标' => ["for a, b in x:\n pass\n", 'case.py:1: unsupported Python syntax For: only a simple for-loop target'], - 'del 不支持的元组目标' => ["x = (1, 2)\ndel (x)\n", 'case.py:2: unsupported Python syntax Delete: unsupported del target'], '模块属性赋值' => ["import sys\nsys.stdout = None\n", 'case.py:2: unsupported Python syntax Attribute: Python module attributes cannot be assigned'], '相对导入' => ["from . import mod\n", 'case.py:1: unsupported Python syntax ImportFrom: relative imports'], '星号导入' => ["from os import *\n", 'case.py:1: unsupported Python syntax ImportFrom: star imports'], - 'main 函数名冲突' => ["def main():\n pass\n", 'case.py:1: unsupported Python syntax FunctionDef: a Python function named main conflicts'], '嵌套函数' => ["def f():\n def g():\n pass\n", 'case.py:2: unsupported Python syntax FunctionDef: nested functions'], - '函数装饰器' => ["@decorator\ndef f():\n pass\n", 'case.py:2: unsupported Python syntax FunctionDef: function decorators'], '同时变长与关键字变长' => ["def f(*a, **kw):\n pass\n", 'case.py:1: unsupported Python syntax FunctionDef: simultaneous *args and **kwargs'], '链式比较' => ["x = 1 < 2 < 3\n", 'case.py:1: unsupported Python syntax Compare: chained comparisons'], 'f-string 转换符' => ['x = 1' . "\n" . 'print(f"{x!r}")' . "\n", 'case.py:2: unsupported Python syntax FormattedValue'], diff --git a/src/PythonTools/Converter/PythonToTypePhpConverter.php b/src/PythonTools/Converter/PythonToTypePhpConverter.php index e77d0844..348215b2 100644 --- a/src/PythonTools/Converter/PythonToTypePhpConverter.php +++ b/src/PythonTools/Converter/PythonToTypePhpConverter.php @@ -15,6 +15,9 @@ final class PythonToTypePhpConverter /** @var array */ private array $definedFunctions = []; + /** @var array 被装饰的函数:调用点必须经变量间接调用装饰结果 */ + private array $decoratedFunctions = []; + /** @var array */ private array $moduleGlobals = []; @@ -41,6 +44,7 @@ final class PythonToTypePhpConverter $this->moduleAliases = []; $this->importedSymbols = []; $this->definedFunctions = []; + $this->decoratedFunctions = []; $this->moduleGlobals = []; $this->indent = 0; $tree = $this->loader->parse($source, $filename); @@ -49,10 +53,20 @@ final class PythonToTypePhpConverter foreach ($tree['body'] ?? [] as $node) { if (in_array($node['_type'] ?? '', ['Assign', 'AnnAssign', 'AugAssign'], true)) { - $targets = ($node['_type'] ?? '') === 'Assign' ? ($node['targets'] ?? []) : [$node['target'] ?? []]; - foreach ($targets as $target) { - if (($target['_type'] ?? '') === 'Name') { - $this->moduleGlobals[(string) $target['id']] = true; + // 纯注解声明没有运行期值,不登记为模块全局变量 + $annotationOnly = ($node['_type'] ?? '') === 'AnnAssign' && ($node['value'] ?? null) === null; + if (!$annotationOnly) { + $targets = ($node['_type'] ?? '') === 'Assign' ? ($node['targets'] ?? []) : [$node['target'] ?? []]; + foreach ($targets as $target) { + // 解构赋值展开为其中的名称元素 + $elements = in_array($target['_type'] ?? '', ['Tuple', 'List'], true) + ? ($target['elts'] ?? []) + : [$target]; + foreach ($elements as $element) { + if (($element['_type'] ?? '') === 'Name') { + $this->moduleGlobals[(string) $element['id']] = true; + } + } } } } @@ -61,10 +75,12 @@ final class PythonToTypePhpConverter $this->collectImport($node); } elseif ($type === 'FunctionDef') { $name = (string) $node['name']; - if ($name === 'main') { - $this->unsupported($node, 'a Python function named main conflicts with the TypePHP entry point'); - } $this->definedFunctions[$name] = true; + if (($node['decorator_list'] ?? []) !== []) { + // 装饰结果绑定到模块级变量,函数内调用需要 global 注入 + $this->decoratedFunctions[$name] = true; + $this->moduleGlobals[$name] = true; + } $functions[] = $node; } else { $main[] = $node; @@ -92,6 +108,12 @@ final class PythonToTypePhpConverter if ($this->moduleGlobals !== []) { $lines[] = $this->line('global ' . implode(', ', $this->variables(array_keys($this->moduleGlobals))) . ';'); } + // 装饰器重绑定先于其他顶层语句执行,使后续调用拿到装饰结果 + foreach ($functions as $function) { + foreach ($this->decoratorRebindings($function) as $rebinding) { + $lines[] = $this->line($rebinding); + } + } foreach ($main as $node) { array_push($lines, ...$this->statement($node)); } @@ -139,10 +161,9 @@ final class PythonToTypePhpConverter 'FunctionDef' => $this->functionDefinition($node), 'Assign' => $this->assignment($node), 'AnnAssign' => ($node['value'] ?? null) === null - ? $this->unsupported($node, 'annotation-only assignments have no TypePHP runtime value') + ? [$this->line('// annotation-only declaration: ' . $this->safeComment((string) ($node['target']['id'] ?? '?')))] : [$this->line($this->target($node['target']) . ' = ' . $this->expression($node['value']) . ';')], - 'AugAssign' => [$this->line($this->target($node['target']) . ' ' . $this->binaryOperator($node['op'], $node) - . '= ' . $this->expression($node['value']) . ';')], + 'AugAssign' => $this->augAssignment($node), 'Expr' => $this->expressionStatement($node), 'Return' => [$this->line('return' . (($node['value'] ?? null) === null ? '' : ' ' . $this->expression($node['value'])) . ';')], 'If' => $this->ifStatement($node), @@ -164,11 +185,8 @@ final class PythonToTypePhpConverter if ($this->indent !== 0) { $this->unsupported($node, 'nested functions require Python closure scope analysis'); } - if (($node['decorator_list'] ?? []) !== []) { - $this->unsupported($node, 'function decorators are not supported yet'); - } $parameters = $this->parameters($node['args'], $node); - $lines = [$this->line('function ' . $node['name'] . '(' . $parameters . ')'), $this->line('{')]; + $lines = [$this->line('function ' . $this->functionName((string) $node['name']) . '(' . $parameters . ')'), $this->line('{')]; $this->indent++; $locals = $this->functionLocalNames($node); $globals = array_values(array_diff(array_keys($this->moduleGlobals), array_keys($locals))); @@ -183,6 +201,59 @@ final class PythonToTypePhpConverter return $lines; } + /** + * Python 的 main 函数与 TypePHP 入口点冲突,重命名为 main_。 + */ + private function functionName(string $name): string + { + return $name === 'main' ? 'main_' : $name; + } + + /** + * 生成装饰器的重绑定语句(Python 自底向上应用装饰器)。 + * 装饰结果存入同名模块变量,调用点经变量间接调用。 + * + * @param array $function @return list + */ + private function decoratorRebindings(array $function): array + { + $decorators = $function['decorator_list'] ?? []; + if ($decorators === []) { + return []; + } + $name = (string) $function['name']; + $lines = []; + foreach (array_reverse($decorators) as $decorator) { + $lines[] = $this->variable($name) . ' = ' . $this->decoratorCallable($decorator) . '(' + . var_export($this->functionName($name), true) . ');'; + } + return $lines; + } + + /** @param array $node */ + private function decoratorCallable(array $node): string + { + // @dec(args):装饰器工厂,先求值再调用其返回值 + if (($node['_type'] ?? '') === 'Call') { + return $this->call($node); + } + if (($node['_type'] ?? '') === 'Name') { + $name = (string) $node['id']; + if (isset($this->importedSymbols[$name])) { + $symbol = $this->importedSymbols[$name]; + return 'python\\' . str_replace('.', '\\', $symbol['module']) . '\\' . $symbol['member']; + } + if (isset($this->definedFunctions[$name])) { + return $this->functionName($name); + } + return $this->variable($name); + } + if (($node['_type'] ?? '') === 'Attribute') { + return $this->attribute($node); + } + return '(' . $this->expression($node) . ')'; + } + /** @param array $arguments @param array $owner */ private function parameters(array $arguments, array $owner): string { @@ -215,14 +286,63 @@ final class PythonToTypePhpConverter /** @param array $node @return list */ private function assignment(array $node): array { - if (count($node['targets'] ?? []) !== 1) { - $this->unsupported($node, 'chained assignments are not supported yet'); + $targets = $node['targets'] ?? []; + foreach ($targets as $target) { + if (in_array($target['_type'] ?? '', ['Tuple', 'List'], true)) { + if (count($targets) > 1) { + $this->unsupported($node, 'destructuring with chained targets is not supported'); + } + // a, b = x → [$a, $b] = $x->toArray(); + return [$this->line($this->destructuringTarget($target, $node) . ' = ' + . $this->iterableValue($node['value']) . ';')]; + } + if (count($targets) > 1 && ($target['_type'] ?? '') !== 'Name') { + $this->unsupported($node, 'chained assignments are only supported for plain name targets'); + } + } + $left = implode(' = ', array_map(fn(array $target) => $this->target($target), $targets)); + return [$this->line($left . ' = ' . $this->expression($node['value']) . ';')]; + } + + /** @param array $node @param array $owner */ + private function destructuringTarget(array $node, array $owner): string + { + $parts = []; + foreach ($node['elts'] ?? [] as $element) { + $parts[] = match ($element['_type'] ?? '') { + 'Name' => $this->variable((string) $element['id']), + 'Attribute', 'Subscript' => $this->target($element), + // PHP 的 list 赋值不支持展开,嵌套元组的元素仍是 PyObject 无法直接解构 + 'Starred' => $this->unsupported($owner, 'starred destructuring is not supported'), + default => $this->unsupported($owner, 'nested destructuring is not supported'), + }; } - $target = $node['targets'][0]; - if (in_array($target['_type'] ?? '', ['Tuple', 'List'], true)) { - $this->unsupported($node, 'destructuring assignments are not supported yet'); + return '[' . implode(', ', $parts) . ']'; + } + + /** @param array $node */ + private function iterableValue(array $node): string + { + $expression = $this->expression($node); + if (!in_array($node['_type'] ?? '', [ + 'Name', 'Attribute', 'Call', 'Subscript', 'List', 'Tuple', 'Set', 'Dict', + ], true)) { + $expression = '(' . $expression . ')'; + } + return $expression . '->toArray()'; + } + + /** @param array $node @return list */ + private function augAssignment(array $node): array + { + $target = $this->target($node['target']); + $operator = $node['op']['_type'] ?? ''; + // PHP 没有 //= 与 @=,展开为对应的运算符函数调用 + if ($operator === 'FloorDiv' || $operator === 'MatMult') { + $function = $operator === 'FloorDiv' ? 'python\\operator\\floordiv' : 'python\\operator\\matmul'; + return [$this->line($target . ' = ' . $function . '(' . $target . ', ' . $this->expression($node['value']) . ');')]; } - return [$this->line($this->target($target) . ' = ' . $this->expression($node['value']) . ';')]; + return [$this->line($target . ' ' . $this->binaryOperator($node['op'], $node) . '= ' . $this->expression($node['value']) . ';')]; } /** @param array $node @return list */ @@ -408,11 +528,25 @@ final class PythonToTypePhpConverter /** @param array $node @return list */ private function deleteStatement(array $node): array { - $lines = []; - foreach ($node['targets'] ?? [] as $target) { + $targets = []; + $walk = function (array $target) use (&$walk, &$targets, $node): void { + // del (a, b) / del [a, b] 逐项展开 + if (in_array($target['_type'] ?? '', ['Tuple', 'List'], true)) { + foreach ($target['elts'] ?? [] as $element) { + $walk($element); + } + return; + } if (!in_array($target['_type'] ?? '', ['Name', 'Attribute', 'Subscript'], true)) { $this->unsupported($node, 'unsupported del target'); } + $targets[] = $target; + }; + foreach ($node['targets'] ?? [] as $target) { + $walk($target); + } + $lines = []; + foreach ($targets as $target) { $lines[] = $this->line('unset(' . $this->target($target) . ');'); } return $lines; @@ -439,6 +573,7 @@ final class PythonToTypePhpConverter 'Lambda' => 'fn (' . $this->parameters($node['args'], $node) . ') => ' . $this->expression($node['body']), 'JoinedStr' => $this->joinedString($node), 'Starred' => '...' . $this->expression($node['value']), + 'NamedExpr' => '(' . $this->variable((string) $node['target']['id']) . ' = ' . $this->expression($node['value']) . ')', default => $this->unsupported($node), }; } @@ -449,11 +584,14 @@ final class PythonToTypePhpConverter $function = $node['func']; if (($function['_type'] ?? '') === 'Name') { $name = (string) $function['id']; - if (isset($this->importedSymbols[$name])) { + if (isset($this->decoratedFunctions[$name])) { + // 装饰结果绑定在同名变量上,必须经变量间接调用 + $callable = $this->variable($name); + } elseif (isset($this->importedSymbols[$name])) { $symbol = $this->importedSymbols[$name]; $callable = 'python\\' . str_replace('.', '\\', $symbol['module']) . '\\' . $symbol['member']; } elseif (isset($this->definedFunctions[$name])) { - $callable = $name; + $callable = $this->functionName($name); } elseif ($this->isPythonBuiltin($name)) { $callable = 'python\\' . $name; } else {