feat(converter): add Python to TypePHP conversion features

- Support chained assignments like x = y = 1
- Implement destructuring assignments using ->toArray() conversion
- Add walrus operator (:=) expression support
- Enable function decorator handling with variable rebinding
- Support floor division //= and matrix multiplication @= operators
- Add tuple/list target expansion for del statements
- Convert pure annotations to comments instead of errors
- Rename main function to main_ to avoid TypePHP entry point conflict
- Generate proper decorator rebindings in main() section
- Handle decorated function calls through variable indirection
- Support stacked decorators with bottom-up application order
- Process decorator factories with argument evaluation
- Add imported symbol decorator support
- Implement proper global variable injection for decorated functions
pull/48/head
韩天峰 2 weeks ago
parent c47ef90593
commit f5ca997e65
  1. 44
      docs/PY2PHP.md
  2. 141
      phpunit/src/PythonTools/PythonToTypePhpConverterTest.php
  3. 186
      src/PythonTools/Converter/PythonToTypePhpConverter.php

@ -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` 注入清单中出现该名字(冗余但合法)。
## 运行测试

@ -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'],

@ -15,6 +15,9 @@ final class PythonToTypePhpConverter
/** @var array<string, true> */
private array $definedFunctions = [];
/** @var array<string, true> 被装饰的函数:调用点必须经变量间接调用装饰结果 */
private array $decoratedFunctions = [];
/** @var array<string, true> */
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<string, mixed> $function @return list<string>
*/
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<string, mixed> $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<string, mixed> $arguments @param array<string, mixed> $owner */
private function parameters(array $arguments, array $owner): string
{
@ -215,14 +286,63 @@ final class PythonToTypePhpConverter
/** @param array<string, mixed> $node @return list<string> */
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<string, mixed> $node @param array<string, mixed> $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<string, mixed> $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<string, mixed> $node @return list<string> */
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<string, mixed> $node @return list<string> */
@ -408,11 +528,25 @@ final class PythonToTypePhpConverter
/** @param array<string, mixed> $node @return list<string> */
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 {

Loading…
Cancel
Save