Python 互调用支持

pull/47/head
韩天峰 2 weeks ago
parent 2be13eefcd
commit 069d3e61bb
  1. 8
      phpunit/code/python/alias-conflict.php
  2. 21
      phpunit/code/python/builtins.php
  3. 7
      phpunit/code/python/constructor-only.php
  4. 8
      phpunit/code/python/duplicate-module.php
  5. 6
      phpunit/code/python/invalid-builtin-path.php
  6. 18
      phpunit/code/python/module-access.php
  7. 12
      phpunit/code/python/module-constant.php
  8. 12
      phpunit/code/python/nested-module.php
  9. 24
      phpunit/code/python/object-protocol.php
  10. 15
      phpunit/code/python/operators.php
  11. 10
      phpunit/code/python/plain-value-arguments.php
  12. 15
      phpunit/code/python/plain-value.php
  13. 8
      phpunit/code/python/unused-module.php
  14. 3
      phpunit/code/typed-object-unset-assign-null.php
  15. 7
      phpunit/src/AssignTest.php
  16. 238
      phpunit/src/Python/PythonModuleTest.php
  17. 936
      python/design.md
  18. 91
      python/implementation-plan.md
  19. 68
      src/CompilerBase.php
  20. 17
      src/Generator/ClosureGenerator.php
  21. 31
      src/Parser/AssignOpTrait.php
  22. 76
      src/Parser/BinaryOpTrait.php
  23. 21
      src/Parser/ClassConstantFetchTrait.php
  24. 5
      src/Parser/FunctionCallTrait.php
  25. 15
      src/Parser/MethodCallTrait.php
  26. 5
      src/Parser/PropertyAccessTrait.php
  27. 4
      src/Parser/TypeConversionTrait.php
  28. 16
      src/Parser/UnaryExpressionTrait.php
  29. 1
      src/Parser/UniversalMethodCall.php
  30. 652
      src/Python/PythonModuleTrait.php
  31. 8
      src/Resolver/DeclarationSymbolTrait.php
  32. 11
      src/Translator.php
  33. 8
      tests/compiler/basic/unset-typed-object-reassign.phpt
  34. 18
      tests/compiler/basic/unset-typed-object-state.phpt
  35. 40
      tests/compiler/python/argument-conversion.phpt
  36. 41
      tests/compiler/python/argument-order-callable.phpt
  37. 29
      tests/compiler/python/builtin-errors.phpt
  38. 54
      tests/compiler/python/builtins.phpt
  39. 22
      tests/compiler/python/case-sensitive-builtin.phpt
  40. 22
      tests/compiler/python/constructor-method-return.phpt
  41. 31
      tests/compiler/python/conversion-errors.phpt
  42. 1
      tests/compiler/python/empty.ini
  43. 17
      tests/compiler/python/lib/typephp_protocol.py
  44. 19
      tests/compiler/python/missing-phpy.phpt
  45. 19
      tests/compiler/python/module-access.phpt
  46. 23
      tests/compiler/python/module-return-object.phpt
  47. 99
      tests/compiler/python/object-protocol.phpt
  48. 149
      tests/compiler/python/operators.phpt
  49. 53
      tests/compiler/python/plain-value.phpt
  50. 13
      tests/compiler/python/unused-module.phpt
  51. 36
      tests/compiler/trait/trait-constant-array-spread.phpt

@ -0,0 +1,8 @@
<?php
use App\Service as NP;
use python\numpy as np;
function main(): void
{
}

@ -0,0 +1,21 @@
<?php
function pythonBuiltins(): void
{
$list = python\list([1, 2, 3]);
$dict = Python\dict(['answer' => 42]);
$tuple = PYTHON\tuple([1, 2]);
$set = python\set([1, 2]);
$str = python\str(123);
$int = python\int('42');
$object = python\object('value');
$bytes = python\bytes('value');
$value = python\len($list);
$scalar = python\scalar($int)->toInt();
python\print($dict, $tuple, $set, $str, $int, $object, $bytes, $value, $scalar);
}
function main(): void
{
}

@ -0,0 +1,7 @@
<?php
function main(): void
{
$list = python\list([42]);
$value = $list[0];
}

@ -0,0 +1,8 @@
<?php
use python\numpy as array_api;
function pythonDuplicateModule(): mixed
{
return array_api::$version;
}

@ -0,0 +1,6 @@
<?php
function main(): void
{
python\collections\deque();
}

@ -0,0 +1,18 @@
<?php
use Python\numpy as np;
use python\unused;
function pythonModuleVersion()
{
return np::$version;
}
function pythonModuleArray()
{
return np::array([1, 2, 3]);
}
function main(): void
{
}

@ -0,0 +1,12 @@
<?php
use python\math;
function pythonModuleConstant(): mixed
{
return math::pi;
}
function main(): void
{
}

@ -0,0 +1,12 @@
<?php
use python\numpy\linalg as linalg;
function pythonNestedModule(): mixed
{
return linalg::norm([3, 4]);
}
function main(): void
{
}

@ -0,0 +1,24 @@
<?php
use Python\sys;
use Python\protocol;
function main(): void
{
sys::$path->append('/tmp');
$object = protocol::make();
$object->name = 'value';
$value = $object->child->method(suffix: '!');
$integer = $object->toInt();
$item = $object[0];
$object[1] = $value;
$object[0] += python\int(1);
$object->counter += python\int(1);
unset($object[2]);
$exists = isset($object[3]);
foreach ($object as $key => $entry) {
echo $key, $entry;
}
$result = $object(1, right: 2);
var_dump($integer);
}

@ -0,0 +1,15 @@
<?php
function main(): void
{
$left = python\int(7);
$right = python\int(3);
$sum = $left + $right;
$reverse = 10 + $right;
$same = $left === $left;
$different = $left !== python\int(7);
$left += 2;
var_dump($sum, $reverse, $same, $different, $left);
}

@ -0,0 +1,10 @@
<?php
function invalidPlainValueCall(PyObject $value): void
{
$value->toPlainValue(1);
}
function main(): void
{
}

@ -0,0 +1,15 @@
<?php
function main(): void
{
$list = python\list([1, 2, 3]);
$plain = $list->toPlainValue();
$dynamic = convertPlainValue($list);
$array = $dynamic->toArray();
var_dump($plain, $array);
}
function convertPlainValue(mixed $value): mixed
{
return $value->toPlainValue();
}

@ -0,0 +1,8 @@
<?php
use python\module_that_does_not_exist;
function main(): void
{
echo "no import\n";
}

@ -6,7 +6,10 @@ class TypedObjectUnsetNullValue
function main(): void
{
$value = new TypedObjectUnsetNullValue();
$value = null;
$value = new TypedObjectUnsetNullValue();
unset($value);
$value = null;
$value = new TypedObjectUnsetNullValue();
}

@ -22,12 +22,9 @@ class AssignTest extends \BaseTest
);
}
public function testCannotAssignNullToTypedObjectAfterUnset()
public function testCanAssignNullToTypedObject()
{
$this->exec(
'Cannot assign null to typed object `$value` of type `TypedObjectUnsetNullValue`; use unset() to clear it',
'typed-object-unset-assign-null.php'
);
$this->compile('typed-object-unset-assign-null.php');
}
public function testCannotAssignUnrelatedObjectToInterfaceDeclaredObject()

@ -0,0 +1,238 @@
<?php
namespace TypePhp\Tests\Python;
use PHPUnit\Framework\TestCase;
use PhpParser\Error;
use TypePhp\CompilerTest;
use TypePhp\Exception\TestError;
final class PythonModuleTest extends TestCase
{
public function testUsedModuleGeneratesLazyZendBindingWithoutPhpyLinkage(): void
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$source = ROOT_PATH . '/phpunit/code/python/module-access.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$cppFile = $compiler->convertFile($source);
$cpp = file_get_contents($cppFile);
$extensionFile = $compiler->genExtension();
$extension = file_get_contents($extensionFile);
$this->assertStringContainsString('php_get_python_module(', $cpp);
$this->assertStringContainsString('.attr(', $cpp);
$this->assertStringContainsString('.call(', $cpp);
$this->assertStringContainsString('THREAD_LOCAL zval php_python_module_map[1]', $extension);
$this->assertStringContainsString('php::Object php_get_python_module(', $extension);
$this->assertStringContainsString('zval_ptr_dtor(', $extension);
$this->assertStringContainsString('PyCore', $extension);
$this->assertStringContainsString('import', $extension);
$this->assertStringNotContainsString('#include <phpy', $extension);
$this->assertStringNotContainsString('phpy::', $extension);
$this->assertStringNotContainsString('module_that_does_not_exist', $extension);
}
public function testUnusedPythonUseDoesNotGenerateModuleRuntimeState(): void
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$source = ROOT_PATH . '/phpunit/code/python/unused-module.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$compiler->convertFile($source);
$extensionFile = $compiler->genExtension();
$extension = file_get_contents($extensionFile);
$this->assertStringNotContainsString('php_python_module_map', $extension);
$this->assertStringNotContainsString('php_get_python_module', $extension);
$this->assertStringNotContainsString('module_that_does_not_exist', $extension);
}
public function testModuleValueCannotUsePhpClassConstantSyntax(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Python module value `math::pi` must use `math::$pi`');
$this->compileFixture('module-constant.php');
}
public function testPythonModuleAliasConflictsCaseInsensitivelyWithClassAlias(): void
{
$this->expectException(Error::class);
$this->expectExceptionMessage('the name is already in use');
$this->compileFixture('alias-conflict.php');
}
public function testNestedModuleUsesPythonDottedImportName(): void
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$source = ROOT_PATH . '/phpunit/code/python/nested-module.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$compiler->convertFile($source);
$extension = file_get_contents($compiler->genExtension());
$this->assertStringContainsString('numpy.linalg', $extension);
$this->assertStringNotContainsString('numpy\\\\linalg', $extension);
}
public function testSameModuleAcrossFilesUsesOneRuntimeSlot(): void
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$sources = [
ROOT_PATH . '/phpunit/code/python/module-access.php',
ROOT_PATH . '/phpunit/code/python/duplicate-module.php',
];
$compiler->addFiles($sources);
foreach ($sources as $source) {
$compiler->prepareFile($source);
$compiler->convertFile($source);
}
$extension = file_get_contents($compiler->genExtension());
$this->assertStringContainsString('THREAD_LOCAL zval php_python_module_map[1]', $extension);
}
public function testPythonBuiltinsUseBuiltinsModuleAndPreserveKnownObjectTypes(): void
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$source = ROOT_PATH . '/phpunit/code/python/builtins.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$cpp = file_get_contents($compiler->convertFile($source));
$extension = file_get_contents($compiler->genExtension());
$this->assertStringContainsString('php_get_python_module(', $cpp);
$this->assertStringContainsString('.call(', $cpp);
$this->assertStringContainsString('php::Object list;', $cpp);
$this->assertStringContainsString('php::Object dict;', $cpp);
$this->assertStringContainsString('php::Object tuple;', $cpp);
$this->assertStringContainsString('php::Object set;', $cpp);
$this->assertStringContainsString('php::Object str;', $cpp);
$this->assertStringContainsString('php::Object _php__var__int;', $cpp);
$this->assertStringContainsString('php::Object object;', $cpp);
$this->assertStringContainsString('php::Object bytes;', $cpp);
$this->assertStringContainsString('scalar = php::toInt(', $cpp);
$this->assertStringContainsString('php::newObject(', $cpp);
$this->assertStringContainsString('PyList', $extension);
$this->assertStringContainsString('PyDict', $extension);
$this->assertStringContainsString('THREAD_LOCAL zval php_python_module_map[1]', $extension);
$this->assertStringContainsString('builtins', $extension);
$this->assertStringContainsString('PyCore::setOptions', $extension);
$this->assertStringContainsString('return_as_object', $extension);
$this->assertStringContainsString('php_python_runtime_configured = false;', $extension);
$this->assertStringNotContainsString('python\\\\list', $extension);
}
public function testPythonBuiltinRejectsNestedModuleSyntax(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Python builtins must use the form `python\\name()`');
$this->compileFixture('invalid-builtin-path.php');
}
public function testConstructorOnlyProgramConfiguresObjectPreservingRuntimeLazily(): void
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$source = ROOT_PATH . '/phpunit/code/python/constructor-only.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$cpp = file_get_contents($compiler->convertFile($source));
$extension = file_get_contents($compiler->genExtension());
$this->assertStringContainsString('php_configure_python_runtime()', $cpp);
$this->assertStringContainsString('void php_configure_python_runtime()', $extension);
$this->assertStringContainsString('return_as_object', $extension);
}
public function testPythonOperatorsLowerToTheOperatorModule(): void
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$source = ROOT_PATH . '/phpunit/code/python/operators.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$cpp = file_get_contents($compiler->convertFile($source));
$extension = file_get_contents($compiler->genExtension());
$this->assertStringContainsString('operator', $extension);
$this->assertStringContainsString('.call(', $cpp);
$this->assertStringContainsString('add', $extension);
$this->assertStringContainsString('iadd', $extension);
$this->assertStringContainsString('is_', $extension);
$this->assertStringContainsString('is_not', $extension);
}
public function testPythonObjectProtocolStaysOnZendDynamicDispatch(): void
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$source = ROOT_PATH . '/phpunit/code/python/object-protocol.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$cpp = file_get_contents($compiler->convertFile($source));
$extension = file_get_contents($compiler->genExtension());
$this->assertStringContainsString('.call(', $cpp);
$this->assertStringContainsString('.attr(', $cpp);
$this->assertStringContainsString('.item(', $cpp);
$this->assertStringContainsString('php::ForeachIterator', $cpp);
$this->assertStringContainsString('integer = php::toInt(object);', $cpp);
$this->assertStringContainsString('iadd', $extension);
$this->assertStringNotContainsString('phpy::', $cpp);
}
public function testToPlainValueUsesTheZendScalarFacade(): void
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$source = ROOT_PATH . '/phpunit/code/python/plain-value.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$cpp = file_get_contents($compiler->convertFile($source));
$extension = file_get_contents($compiler->genExtension());
$this->assertStringContainsString('php::Var plain;', $cpp);
$this->assertStringContainsString('php::Var dynamic;', $cpp);
$this->assertStringContainsString('php::toPlainValue(', $cpp);
$this->assertStringContainsString('php::Var value', $cpp);
$this->assertStringNotContainsString('.call("toPlainValue"', $cpp);
$this->assertStringNotContainsString('phpy::', $cpp);
}
public function testToPlainValueRejectsArguments(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('The toPlainValue method does not accept parameters');
$this->compileFixture('plain-value-arguments.php');
}
private function compileFixture(string $file): void
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$source = ROOT_PATH . '/phpunit/code/python/' . $file;
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$compiler->convertFile($source);
}
}

@ -0,0 +1,936 @@
# TypePHP 与 Python 语言级互调用设计
> 状态:核心设计已确认,按 `python/implementation-plan.md` 分阶段实施。
>
> 本文是语法、类型语义、运行时边界和兼容性目标的设计规范;尚未确认的细节继续在文末维护。
## 1. 目标
TypePHP 应在语言层面提供从 TypePHP 调用 Python 包的能力:
1. TypePHP 导入 Python 模块,访问模块成员,调用 Python 函数和类。
2. TypePHP 操作 Python 对象,包括属性、方法、下标、迭代、运算符和调用协议。
3. TypePHP 函数、闭包和对象可以作为 Python 调用的参数,并允许 Python 在该次动态调用关系中同步回调。
4. 两个 VM 在同一进程内直接互调用,不通过 JSON、RPC 或子进程。
5. 默认保留 Python 对象身份和类型信息,避免不必要的深拷贝。
6. 语法面向普通 TypePHP/PHP 开发者,常规调用不要求理解 CPython C API、GIL 或引用计数。
7. 本功能是可选的扩展级能力;不使用 Python 语法的项目不依赖 phpy。
其中最主要的语言变化是 `use python\...`:它把 phpy 原本需要手写的 `PyCore::import('module')` 和返回变量提升为编译期可识别的模块别名。Python 对象的属性、方法、下标、迭代、参数转换、返回包装和异常等能力原则上复用 phpy 已有实现,不在 TypePHP 中重新建立一套运行时。
非目标:
- 不编译 Python 源码,也不试图替代 CPython。
- 不承诺将动态 Python API 静态类型化。
- 永久不支持 Python 线程、`asyncio` 或 CPython subinterpreter。
- 不生成 Python extension,不向 Python 注册 TypePHP 函数、类或模块。
- 不提供 `#[PythonExport]` 或其他 TypePHP 符号导出机制。
- 不追求兼容 Python 语法;目标是让 TypePHP 程序方便、可靠地调用 Python 包。
- 不将任意 Python 容器自动、递归地复制为 TypePHP 数组。
## 2. 参考设计
### 2.1 Mojo
Mojo 使用未经修改的 CPython 运行时保证 Python 生态兼容性,并用统一的 `PythonObject` 包装动态 Python 值。TypePHP 只借鉴其嵌入和对象包装设计,不采用其导出机制。
可借鉴的部分:
- Python 值默认保持为包装对象。
- TypePHP 基础值传入 Python 时可自动转换。
- Python 值转回 TypePHP 原生类型时显式转换。
- 动态 Python 值使用统一代理类型承载。
参考:[Mojo Python interoperability](https://docs.modular.com/stable/mojo/manual/python/)、[Mojo Python types](https://docs.modular.com/mojo/manual/python/types)。
### 2.2 pybind11
pybind11 明确区分对象所有权、返回值策略、解释器生命周期、GIL guard、位置参数和关键字参数。其经验说明:跨语言调用最危险的部分不是调用语法,而是对象生命周期和异常路径。
TypePHP 不应把 pybind11 的所有权策略暴露给普通用户,但运行时必须建立同等严格的内部契约。
参考:[pybind11 embedding](https://pybind11.readthedocs.io/en/stable/advanced/embedding.html)、[pybind11 functions](https://pybind11.readthedocs.io/en/stable/advanced/functions.html)。
### 2.3 PyO3
PyO3 使用 GIL token 和带生命周期的 Python 对象指针,从类型系统上区分持有对象、借用对象和 GIL 绑定对象。
TypePHP 无需向用户暴露生命周期参数,但 phpy 的 C++ 层应借鉴这一点:所有 CPython API 调用必须能证明当前持有 GIL,所有 `PyObject*` 必须明确是 owned、borrowed 还是 stolen reference。
参考:[PyO3 object model](https://pyo3.rs/main/doc/pyo3/)、[PyO3 Python object types](https://pyo3.rs/main/types)。
## 3. phpy 的定位
phpy 是本功能的运行时基础候选,而不是已经验证完成的稳定依赖。
可复用能力包括:
- 在 ZendVM 进程内初始化 CPython。
- `zval``PyObject*` 的边界转换。
- Python 模块、对象、字符串、序列、字典、集合、迭代器和 callable 的代理对象。
- TypePHP/PHP 闭包传入 Python后的 callable 代理。
- Python 异常到 Zend 异常的基础映射。
- Python 同步调用由 ZendVM 主动传入的函数、对象和 callable 代理的基础设施。
- GIL RAII guard 的雏形。
但是不能直接假定现有实现完全正确。后续实施必须同时 review phpy、重构边界、增加测试、修复 BUG 和优化性能。
设计阶段已经识别出的重点审计项:
- CPython 初始化、重复初始化、关闭顺序和仍存活对象的析构。
- 每个 CPython API 的 owned/borrowed/stolen reference 规则。
- 所有成功路径和异常路径的 `Py_INCREF/Py_DECREF` 对称性。
- GIL 获取、重入调用和 TypePHP 回调 Python 再回调 TypePHP 的行为。
- 转换过程已改为每次顶层转换创建独立的 C++ 转换器对象;转换策略、递归栈和深度限制均为对象内状态,并由 RAII 恢复,不再使用进程级或线程级临时函数指针。仍需继续审计跨 VM 回调和生命周期边界。
- Python 异常转 Zend 异常后,CPython error indicator 是否始终被正确清理。
- Zend 异常转 Python 异常时,原始异常类型、消息和 traceback 的保存。
- 运算符协议是否正确。例如 PHP `/` 不能映射为 Python floor division。
- Python 大整数、无效 UTF-8、包含 NUL 的 bytes、递归容器和循环引用。
- Python 代理持有 Zend 对象时,Zend GC 与 CPython GC 之间可能形成的跨 VM 引用环。
- Python 线程、`asyncio`、subinterpreter 必须被永久、显式拒绝,而不是产生未定义行为。
TypePHP 通过 ZendVM 动态调用 phpy 扩展公开的 `PyCore`、`PyObject`、`PyDict` 等 Facade,不直接链接 `libphpy.so`,也不生成任何 phpy C++ 符号引用。现有公开名称必须保留,TypePHP 不建立第二套用户可见命名体系。
职责边界:
- phpy 负责所有运行时问题:CPython 初始化、GIL、引用计数、对象代理、类型转换、异常和双 VM 生命周期。
- phpy 负责提供稳定、可测试的 Zend internal class/function/object-handler API。
- TypePHP 只负责识别语言语法、静态类型和求值顺序,并生成基于 `zend_function*` 与 PHPX/Zend 通用对象 API 的动态调用。
- TypePHP 不直接操作裸 `PyObject*`,不复制 phpy 的 GIL、引用计数或异常实现。
- 修复运行时 BUG 时优先修复 phpy,不能只在 TypePHP 生成代码中增加补丁绕过。
最小适配原则:
- TypePHP 的核心新增能力是 Python `use` 解析、模块别名符号和对应代码生成。
- `python\name()`、`module::$name`、`module::name()` 和运算符 lowering 都应落到 phpy 的 Zend Facade;Python 运算符通过标准库 `operator` module 调用完整的 CPython 运算协议。
- phpy 已正确解决的行为只补测试并复用;只有 review 或测试证明存在 BUG、隐式转换不符合 TypePHP 规则,或者缺少 Zend 动态入口时,才修改 phpy。
- TypePHP 不实现 CPython 协议细节,不在生成代码中复制 `PyCore`、`PyObject` 或 `PyModule` 的逻辑。
## 4. 可选扩展与运行时检测
Python 互调用是扩展级特性,不是 TypePHP 核心程序的强制依赖。
- TypePHP 生成代码只依赖 ZendVM/PHPX,不 include phpy 头文件,也不链接 `libphpy.so`
- 编译器识别 Python 语法并保留逻辑上的 `PyObject` 类型信息,但不检查 phpy SDK、动态库、ABI 或 Python module 是否存在。
- phpy 必须像普通 PHP 扩展一样由运行环境加载并注册 `PyCore`、`PyObject` 等 Zend internal classes。
- 首次实际使用 Python 符号时,TypePHP 通过 class map/func map 解析 `PyCore` 和对应的 `zend_function*`
- phpy 未加载时,Zend class lookup 抛出可捕获的 PHP `Error`;若未捕获,则按普通 PHP 规则成为 fatal error。
- phpy 已加载但 Python module 不存在时,`PyCore::import()` 通过 phpy 抛出 `PyError`
- 只有 `use python\sys` 而没有实际访问任何 Python 符号时,不发生运行时解析,因此即使没有安装 phpy 也不会报错。
这种模型使同一个 TypePHP 二进制可以在未安装 phpy 的环境中运行不涉及 Python 的路径,也避免 TypePHP 与 phpy 建立原生 C++ ABI 依赖。
### 4.1 TypePHP 代码隔离
TypePHP 中所有 Python 专用实现必须集中到独立子目录,暂定为:
```text
src/Python/
```
该目录负责:
- `python` 特殊根命名空间识别。
- import/module symbol 表。
- Python Zend class/method 名称和逻辑返回类型映射。
- Python 语法糖和静态返回类型映射。
- Python 调用、属性、下标、迭代和运算符的 C++ lowering。
- Python 专用诊断。
通用 Parser、TypeSystem、Optimizer 和 Generator 只允许保留最小、稳定的扩展入口,不应散落 `if ($isPython...)` 特判。Python 功能未启用时,不加载 Python 专用分析器,也不改变现有代码生成路径。
测试同样独立组织,建议使用:
```text
phpunit/src/Python/
phpunit/code/python/
tests/compiler/python/
```
具体目录名在 coding 计划阶段确认,但“实现与测试隔离”是设计约束。
## 5. 总体运行时模型
采用以下模型:
- 一个进程内同时存在一个 ZendVM 和一个 CPython 主解释器。
- CPython 完全通过 phpy 已有的扩展生命周期初始化和关闭;TypePHP 不建立第二套初始化路径。
- 所有 Python API 边界自动获取 GIL,普通用户不操作 GIL。
- `PyObject` 及其 `PyDict`、`PyList`、`PyStr` 等子类持有 CPython strong reference。
- Python 代理对象复制时增加引用计数,析构时在合法的解释器/GIL 上下文中减少引用计数。
- borrowed reference 只允许存在于 phpy 内部的短生命周期作用域,不暴露给 TypePHP。
- TypePHP 调用 Python、Python 同步回调由 TypePHP 作为参数传入的 callable、该 callable 再调用 Python,必须支持同步重入。
- Python 不能独立导入 TypePHP 应用,也不能通过全局注册表查找 TypePHP 函数或类型。
解释器关闭前必须先释放所有由 TypePHP 持有的 Python 对象。不能依赖 `Py_Finalize()` 自动修复错误的生命周期。
## 6. 导入语法
`python` 是编译器识别的保留根命名空间:
```php
use python\sys;
use Python\numpy as np;
use python\numpy\linalg as linalg;
```
分别等价于:
```python
import sys
import numpy as np
import numpy.linalg as linalg
```
在现有 phpy PHP API 中,语义上对应:
```php
$sys = PyCore::import('sys');
$np = PyCore::import('numpy');
$linalg = PyCore::import('numpy.linalg');
```
`PyCore::import()` 返回一个 `PyModule`/`PyObject` 变量,后续属性和方法均通过该变量访问。TypePHP 的 `use python\module` 本身只建立“别名 → Python module 完整名称”的 namespace 标记,不立即执行导入,也不生成 ZendVM class、namespace 或用户可见变量。
当编译器在函数代码中发现 `module::$attr``module::func()` 时,采用与现有 `funcMap` 相同的编译器结构:为实际使用的完整 module 名称分配整数 ID,生成统一的 `THREAD_LOCAL` zval array,并通过 lazy getter 动态调用 `PyCore::import()`。下列名称只是设计示意:
```cpp
THREAD_LOCAL zval php_python_module_map[module_count];
php::Object php_get_python_module(int module_id, const php::Str &module_name)
{
zval *module = &php_python_module_map[module_id];
if (UNEXPECTED(Z_ISUNDEF_P(module))) {
// Resolve PyCore::import through classMap/funcMap and invoke zend_function*.
php::Variant value = php::call(/* cached zend_function* */, php::ArgList{module_name});
ZVAL_COPY(module, value.ptr());
}
return php::Object(module);
}
```
对应 lowering:
```text
use Python\numpy as np
-> compile-time namespace marker: np => "numpy"
-> module id allocated only when np is actually referenced
np::$version
-> php::Object(php_get_python_module(module_id, "numpy")).attr("version")
np::array($value)
-> php::Object(php_get_python_module(module_id, "numpy")).call("array", converted($value))
```
同一完整 module 名称在整个 TypePHP 构建中只分配一个 ID。如果当前 `.php` 文件只有 `use python\sys`,但没有出现任何 `sys::$attr`、`sys::func()` 或其他 `sys` 符号访问,则编译器不为它分配 module ID,运行时不调用 `import('sys')`,也不会因为 Python 环境缺少该 module 而报错。
未使用 module 不触发任何 phpy 运行时解析。`tpc` 只检查 `use python\sys` 本身的语法和别名冲突,不检查 phpy SDK/ABI,也不增加 phpy 链接依赖。
### 6.1 与 `funcMap` 的关系
`pythonModuleMap` 复用 `funcMap` 已验证的整体模式:
- 编译期使用 `完整 module 名称 → integer ID` 的 map 去重。
- 数据声明集中生成,普通 `.cc` 只引用 extern array 和 getter。
- getter 首次访问时初始化,后续通过数组直接命中。
- 只为真正出现成员访问或调用的 module 分配 ID。
- 在应用/request clean 阶段集中清理。
但是两者不能机械地使用完全相同的清理代码:
- `funcMap` 保存由 Zend function table 拥有的 non-owning `zend_function*`,清理时可以直接 `memset`
- `pythonModuleMap` 保存 phpy 返回的 Zend `PyModule` object zval,不能直接 `memset` 覆盖有效对象。
- request clean 必须逐项执行 `zval_ptr_dtor()` 并恢复为 `UNDEF`,让 phpy 自己的 Zend object destructor 处理 Python reference 和 GIL。
- import 失败时 slot 保持 `UNDEF`,不能缓存异常值或半初始化对象。
清理由 TypePHP 使用普通 Zend zval API 完成,不调用 phpy C++ 符号:
```cpp
for (zval &module : php_python_module_map) {
if (!Z_ISUNDEF(module)) {
zval_ptr_dtor(&module);
ZVAL_UNDEF(&module);
}
}
```
TypePHP 只释放 Zend object;其内部 Python 引用计数、GIL 和 error state 仍由 phpy object handler 负责。
### 6.2 `sys.modules` 仍是全局事实来源
Python import 本身就是全局的。getter 首次调用底层 import 时,CPython 从 `sys.modules` 返回已加载 module 或执行首次加载。`pythonModuleMap` 不是第二套 import 系统,只相当于 Python 文件执行 `import numpy as np` 后保存在该文件 namespace 中的绑定:
```text
php_get_python_module(id, "numpy")
-> TypePHP request 内的 PyModule zval binding
-> CPython sys.modules(全局 module identity 与加载状态)
```
它避免每次函数调用都重复进入 Python import API,同时不承担包查找、加载或 reload 逻辑。即使同一个 module 被多个 TypePHP 文件以不同别名引用,只要完整 module 名称相同,就使用同一个 ID 和 `PyModule` Zend object zval。
该绑定与 Python 普通 import 一致:Python 代码之后删除或替换 `sys.modules['numpy']`,不会自动改变已经完成的 `np` 绑定;显式执行 `PyCore::import('numpy')` 则按调用当时的 `sys.modules` 状态处理。
规则:
- `python` 根命名空间的大小写不敏感,`python`、`Python`、`PYTHON` 均识别为同一个语言符号。
- 只有根命名空间不区分大小写。后续模块路径、成员、方法和关键字参数名称严格区分大小写。
- `use python\...` 只能导入 Python 模块。
- 是否存在该模块只能在运行时由 CPython 判断。
- 不支持 `from package import *`
- 初版不设计单独的 `from package import name` 语法,成员统一通过模块别名访问。
- `python` 根命名空间本身不可作为普通 TypePHP/PHP 命名空间声明。
- 模块别名不能与当前文件中的 TypePHP 类、命名空间导入或其他 Python 模块别名冲突。
- 用户仍可直接调用 `PyCore::import()` 并把返回的 `PyModule` 保存到普通变量;`use python\...` 是使用 `pythonModuleMap` lazy binding 的语言级 namespace 标记。
示例:
```php
python\len($value); // 正确
Python\len($value); // 正确,根命名空间大小写不同
python\Len($value); // 错误,Python builtin 名称大小写错误
Python\Len($value); // 错误
```
`python` 不是普通运行时命名空间。它由 TypePHP 编译器识别并转换为 Python 语言符号,因此不会进行普通 PHP 命名空间函数或类查找。
## 7. 模块成员
Python 不区分“类常量”“静态属性”和“模块变量”。模块中的所有名称本质上都是属性。
### 7.1 包变量
读取 Python 包变量使用 PHP 静态属性形式 `module::$name`
```php
use python\math;
use python\os;
use python\numpy as np;
$pi = math::$pi;
$environ = os::$environ;
$arrayType = np::$ndarray;
```
这里的 `$pi`、`$environ` 和 `$ndarray` 是静态成员语法中的成员名,不是读取同名 TypePHP 局部变量。编译器将其 lowering 为 Python module attribute lookup。
不允许使用 `math::pi` 读取包变量。PHP 语法会把它理解为常量访问,而 Python module 没有与 PHP class constant 对应的常量概念。编译器发现 Python module alias 后使用 `module::name` 时,应给出有针对性的 FatalError,并提示改用 `module::$name`
### 7.2 包函数和类构造
调用 Python 包中的 callable 使用 `module::name(...)`
```php
$a = np::array([1, 2, 3]);
$b = np::array([4, 5, 6]);
$c = np::add($a, $b);
```
编译器读取 module 的 `name` 属性,并调用得到的 Python 对象。该对象可以是:
- Python 函数。
- Python class,此时调用执行该类的构造过程并返回实例。
- 实现 `__call__` 的其他 Python 对象。
TypePHP 不需要也不能仅根据 `np::array()` 的语法判断它是函数还是类构造;可调用性由 Python 在运行时判断。成员不存在时产生 Python `AttributeError`,成员不可调用时产生 Python `TypeError`,并统一映射为 `PyError`
待确认:初版是否允许对模块属性赋值,例如 `module::$name = $value`。建议初版只支持读取;需要写入时使用:
```php
python\setattr(os, 'name', $value); // 伪代码,具体模块值语法仍需确定
```
## 8. Python 内置函数与 phpy 语法糖
`python\name()` 表示调用 Python builtins:
```php
python\print('hello'); // 等价于 PyCore::print('hello')
$length = python\len($value)->toPlainValue()->toInt();
$range = python\range(0, 10);
$type = python\type($value);
```
它不是普通 TypePHP 命名空间函数。编译器使用 class/func map 解析 `PyCore` 对应的 `zend_function*` 并动态调用,运行时语义与直接编写对应 `PyCore` 调用一致。
名称严格区分大小写。对于编译器内建映射中已知的错误名称,可以在编译期报错;其他动态 builtin lookup 失败时产生 Python `AttributeError`
一部分名称是现有 phpy 类型构造器的语法糖,而不是直接调用同名 Python builtin:
| TypePHP 语法 | 等价 phpy API |
|---|---|
| `python\dict($array)` | `new PyDict($array)` |
| `python\list($array)` | `new PyList($array)` |
| `python\tuple($array)` | `new PyTuple($array)` |
| `python\set($array)` | `new PySet($array)` |
| `python\str($value)` | `new PyStr($value)` |
| `python\object($value)` | `new PyObject($value)` |
| `python\print(...)` | `PyCore::print(...)` |
| `python\scalar($value)` | `PyCore::scalar($value)` |
例如:
```php
$dict1 = new PyDict([1, 2, 3, 4]);
$dict2 = python\dict([1, 2, 3, 4]);
```
二者必须具有完全相同的运行时语义。这里不能简单转发 CPython `dict([1, 2, 3, 4])`,因为原生 Python builtin 会把参数解释为 key/value pair iterable,与 `PyDict` 的 PHP array 构造规则不同。
所有语法糖的映射必须形成封闭、经过测试的表,不能仅凭函数名猜测。
该映射同时决定编译期静态类型:
```php
$list1 = new PyList();
$list2 = python\list();
$dict1 = new PyDict();
$dict2 = python\dict();
```
- `$list1``$list2` 都是 `PyList` typed object。
- `$dict1``$dict2` 都是 `PyDict` typed object。
- 两种写法必须使用相同的类型检查、方法解析和 Native Call 优化。
- 语法糖不能退化成 `mixed`、`var` 或只有基础类型 `PyObject`
- Python builtin 调用同样遵守对象保持规则,例如 `python\len()` 返回包装 Python int 的 `PyObject`;需要先以 `toPlainValue()`(或兼容入口 `python\scalar()`)离开 Python 对象规则,再使用普通 TypePHP 转换得到确定类型。`python\print()` 的 Python `None` 结果也保持为 `PyObject`,作为独立语句使用时可直接丢弃。
- `toPlainValue()``python\scalar()` 都不是普通 Python builtin 调用,而是明确要求退出 Python 类型规则的转换边界,因此返回 TypePHP `var`
- 动态 Python module 成员调用统一返回 `PyObject`
## 9. Python 对象类型
所有无法在编译期确定静态类型的 Python 值统一表示为:
```php
PyObject
```
`PyObject` 是现有 phpy 的公开类型,也是 TypePHP 的正式运行时类型。不会再引入 `python\Object``python\Any`
Python 内建类型继续使用 phpy 已有的具体代理类,例如 `PyDict`、`PyList`、`PyTuple`、`PySet`、`PyStr`、`PyType`、`PyFn` 和 `PyIter`。这样普通 PHP 与 TypePHP 用户看到的是同一套类型体系。
Python 的 `None` 也是一个合法 Python 对象。它与 TypePHP `null` 的自动转换规则需要单独定义,不能通过空指针表示 Python `None`
## 10. 对象操作
### 10.1 属性和方法
```php
$env = os::$environ;
$items = $env->items();
$name = $object->name;
$object->name = 'new value';
unset($object->name);
```
分别映射为 Python 的 `getattr`、call、`setattr` 和 `delattr` 协议。
`PyObject` 与普通 Object 遵循相同的方法解析规则。TypePHP 不为它保留或注入 `toInt()`、`toFloat()`、`toBool()`、`toString()`、`toArray()` 等特殊转换方法;同名 Python 成员仍按正常的动态成员规则调用。
`toPlainValue()` 是与 `toArray()`、`toString()` 同级的 TypePHP 全局关键词方法,用于把扩展对象转换为 PHP 内置值;当前第一个受支持的扩展对象是 `PyObject`。从 Python 对象进入 TypePHP 原生值时,推荐使用这个可保持链式调用的入口。`python\scalar()` 保留为等价的函数式入口。其返回值再使用普通 TypePHP 转换方法确定类型:
```php
$pyValue = np::int64(42); // PyObject
$value = $pyValue->toPlainValue()->toInt(); // TypePHP int
```
这里的 `toInt()` 作用于 `toPlainValue()` 已返回的 TypePHP 值,并非作用于 `PyObject`
### 10.2 下标
```php
$value = $object[$key];
$object[$key] = $value;
unset($object[$key]);
isset($object[$key]);
```
分别映射到 Python mapping/sequence protocol。
`isset()` 保持 PHP 的空值语义:键或索引不存在时返回 `false`,对应值为 Python `None` 时也返回 `false`。运行时只把 `KeyError` / `IndexError` 识别为“缺失”;Python protocol 抛出的其他异常必须继续映射为 `PyError`,不得被 `isset()` 吞掉。list 和 tuple 的整数下标遵循 Python 负索引规则。
### 10.3 调用对象
```php
$result = $callable($arg1, $arg2);
```
运行时使用 `PyObject_Call`。不可调用对象产生 Python `TypeError`,并映射为 TypePHP 可捕获的 Python 异常。
### 10.4 迭代
```php
foreach ($pythonIterable as $value) {
// Python __iter__ / __next__
}
```
带 key 的形式:
```php
foreach ($pythonIterable as $index => $value) {
}
```
通用 Python iterator 使用从 `0` 开始的 TypePHP 迭代序号作为 `$index`,`$value` 是 `__next__()` 产出的对象。`PyDict` 是 phpy 的专用 mapping wrapper,带 key 的 `foreach` 使用 PHP mapping 习惯:`$index` 是 dict key,`$value` 是对应 dict value。`__iter__()` / `__next__()` 的 Python 异常必须传播为 `PyError`,不能当作正常迭代结束。
## 11. 参数与关键字参数
普通参数按从左到右顺序求值,然后构造 Python positional args:
```php
$model = AutoModel::from_pretrained(
'model-name',
trust_remote_code: true,
device_map: 'auto',
);
```
TypePHP 命名参数映射为 Python keyword arguments。参数名严格区分大小写。
PHP/TypePHP 数组展开规则可用于构造位置参数和关键字参数,但必须满足:
- 整数 key 生成 positional argument。
- 字符串 key 生成 keyword argument。
- positional argument 不能出现在 keyword argument 之后。
- 重复 keyword 产生 Python `TypeError`
是否增加显式的 `python\args()` / `python\kwargs()` 类型,留待后续讨论;初版尽量复用现有调用和数组展开语法。
## 12. 显式转换原则
TypePHP 不继承 phpy 在 ZendVM Facade/opcode 层面的返回值隐式转换行为。语言层采用“参数进入 Python 边界时自动转换、Python 返回值保持对象、返回 TypePHP 时显式转换”的原则。
允许自动转换的场景必须由语法明确指出正在进入 Python:
- `python\name(...)`
- Python module 调用,例如 `np::array(...)`
- `PyObject` 的方法或 callable 调用。
- 显式 Python 容器构造,例如 `new PyList(...)``python\list(...)`
- 参数声明要求 `PyObject`、`PyDict` 等 phpy 类型。
- `PyObject` 与 TypePHP 值组成的混合运算表达式。
在这些调用边界内,所有参数表达式先严格按照 TypePHP 从左到右的顺序求值,再转换为 Python 能接受的对象。TypePHP 标量转换为对应 Python scalar;TypePHP 数组递归转换为 Python list/dict,这一过程会产生深拷贝。这不应扩散为不含 Python 对象的普通 TypePHP 表达式中的全局隐式转换。
“所有参数自动转换”只适用于转换表明确支持的 TypePHP 类型;resource 或其他没有 Python 表示形式的值必须抛出清晰的类型错误,不能静默转换或传递无效指针。
以下场景不允许隐式转换:
- 将 `PyObject` 直接赋给 `int`、`float`、`bool`、`string` 或 `array`
- 将 Python 容器隐式深拷贝成 TypePHP array。
- 因算术、比较或字符串上下文而擅自把 Python 对象变成 TypePHP 标量。
- 根据运行时 Python 类型改变 TypePHP 变量的静态类型。
`echo $pyObject` 可继续兼容现有 `PyObject::__toString()`,但这只属于输出协议,不能被编译器当作一般的字符串隐式转换。
## 13. TypePHP 到 Python 的转换
Python 调用边界允许以下自动转换:
| TypePHP | Python | 语义 |
|---|---|---|
| `null` | `None` | 单例,不是空 `PyObject*` |
| `bool` | `bool` | 值转换 |
| `int` | `int` | Python 任意精度整数 |
| `float` | `float` | double |
| `string` | `str` | 要求合法 UTF-8 |
| list array | `list` | 递归复制 |
| map array | `dict` | 递归复制 |
| `PyObject` 及其子类 | 原对象 | 零拷贝,只传递引用 |
| TypePHP callable | Python callable proxy | Python 可同步回调 TypePHP |
| TypePHP object | Zend object proxy | 不自动复制对象属性 |
PHP array 使用 `zend_array_is_list()` 一类规则决定转换为 Python `list` 还是 `dict`。空数组默认转换为 Python `list`;如需空 dict,必须提供显式构造 API。
数组和普通 TypePHP 字符串每次进入 Python 边界都可能产生分配与复制。文档和性能诊断应建议高频调用、循环调用或大数据场景尽早构造并复用 `PyDict`、`PyList`、`PyStr` 等原生 Python 代理类型,避免重复深拷贝。`PyObject` 及其子类进入 Python 边界时只传递原对象引用,不做内容复制。
推荐写法:
```php
// 只转换一次,后续调用传递同一个 Python 对象。
use python\processor;
$pyItems = python\list($items);
for ($i = 0; $i < 1000; $i++) {
processor::consume($pyItems);
}
```
应避免在循环中反复把同一个 TypePHP 容器作为参数传入,因为每次跨越 Python 调用边界都会重新深拷贝:
```php
for ($i = 0; $i < 1000; $i++) {
processor::consume($items);
}
```
字符串与 bytes 必须区分。TypePHP `string` 默认映射到 Python `str`;二进制内容使用显式 `python\bytes()`
递归数组、循环引用和超深嵌套必须检测并抛出异常,不能无限递归。
## 14. Python 到 TypePHP 的转换
### 14.1 默认规则
TypePHP 的 Python 专用调用路径必须关闭 phpy 的返回值隐式转换,所有 Python 函数、方法、构造调用和运算结果均保持为 phpy 对象。动态调用的静态返回类型统一为 `PyObject`,不能因为运行时结果恰好是 Python `bool`、`int`、`float`、`str`、`list` 或 `dict` 就隐式转换为 TypePHP 值。
当前实现由生成代码在首次实际执行 Python 表达式时,动态调用 `PyCore::setOptions(['return_as_object' => true])`。该初始化是请求级 lazy guard:只写 `use python\module` 而不访问 Python 符号不会触发 phpy;constructor-only 程序也会在构造前完成配置;request clean 会重置 TypePHP 自身的 guard。后续若 phpy 提供无全局模式的对象保持型独立入口,可在不改变语言语义的前提下替换这一运行时实现。
编译器已知的 phpy 构造语法糖仍保留精确子类,例如 `python\list()` 返回 `PyList`、`python\dict()` 返回 `PyDict`;这些类型本身都是 `PyObject` 子类,不构成返回值隐式转换。
phpy Zend Facade 应提供相互独立的“保持 Python 对象”和“显式转换为 TypePHP”入口。不能通过修改进程级全局函数指针或全局转换模式来临时切换,否则嵌套调用、同步重入和异常路径可能把错误策略泄漏给后续调用。TypePHP 生成的普通 Python 调用只动态调用对象保持入口;`toPlainValue()` 与 `python\scalar()` 最终都调用明确的标量转换入口。
phpy 内部已使用 `PythonToPhpConverter``PhpToPythonConverter` 实现这一约束。每次顶层转换拥有独立实例,递归子值复用同一实例;容器进入与退出由 RAII guard 管理,循环容器和超过深度限制的输入会抛出 `PyError`,不会污染后续转换或导致进程崩溃。
原因:
- 保留 Python 对象身份和精确类型。
- 避免容器返回时立即深拷贝。
- Python `int` 可能超过 TypePHP `int` 范围。
- Python 类型的子类可能重载协议,不能按基础容器强制展开。
- 避免 phpy 当前“部分标量自动转换、部分对象保留包装”的行为进入 TypePHP 静态类型系统。
### 14.2 显式转换
Python 对象只有通过 `toPlainValue()`、`python\scalar()`(或手写等价的 `PyCore::scalar()`)才能进入 TypePHP 类型规则:
```php
$nativeValue1 = PyCore::scalar($value);
$nativeValue2 = python\scalar($value); // 完全等价的语法糖
$nativeValue3 = $value->toPlainValue(); // 推荐的链式关键词方法
$integer = $value->toPlainValue()->toInt();
$float = $value->toPlainValue()->toFloat();
$boolean = $value->toPlainValue()->toBool();
$string = $value->toPlainValue()->toString();
$array = $value->toPlainValue()->toArray();
```
规则:
- 编译器把 `toPlainValue()``python\scalar()` 识别为 Python/TypePHP 边界;其后的 `toInt()` 等调用是 TypePHP 原生值已有的普通转换能力。
- `toPlainValue()` 注册在 TypePHP 全局 `KEYWORD_METHOD_MAP`,不属于 Python module 语法。它按 `toArray()` / `toString()` 的 PHPX 自由函数模式生成 `php::toPlainValue(value)`。参数可以是 `php::Object``php::Var`;当前 PHPX 在运行时确认对象是 `PyObject`,再通过 Zend API 调用 `PyCore::scalar()`,不链接 phpy C++ 符号。后续其他扩展类可继续在 PHPX 中增加适配。
- 显式转换完成后,结果完全进入 TypePHP 的静态类型、运算符和参数传递规则,不再采用 Python protocol。
- 容器转换属于显式深转换,并检测递归引用。
- Python 大整数不能静默溢出;现有转换规则需要 review 后再确定与 TypePHP `BigInt` 的精确映射。
- Python `str``bytes` 必须区分,不能都无条件转换为 TypePHP string。
- phpy 只负责 `PyCore::scalar()` 的 Python 到 PHP 值转换;后续 `toInt/toFloat/toBool/toString/toArray` 不属于 phpy,也不应在 `PyObject` 上重复实现。
现有 phpy 的 PHP 用户仍可保留兼容行为;TypePHP 调用 phpy 的对象保持型 Zend API。为此可以重构或新增 phpy internal class method,但不增加 TypePHP 到 phpy 的 C++ 链接依赖。
## 15. 通过 Python `operator` module 实现运算符
对于 `PyObject` 及其子类:
- `+ - * / % ** << >> & | ^` 映射为 Python 标准库 `operator` module 的对应函数。
- `/` 映射 `operator.truediv()`,不能映射 `operator.floordiv()`
- Python floor division 暂用 `python\floordiv($a, $b)`,因为 TypePHP 没有 `//` 运算符。
- `== != < <= > >=` 分别映射 `operator.eq/ne/lt/le/gt/ge()`
- `===` / `!==` 分别映射 `operator.is_()` / `operator.is_not()`
- `if ($object)`、`!$object` 使用 `operator.truth()`
- compound assignment 映射 `operator.iadd/isub/...()`,并用返回对象更新左值。
基础映射:
| TypePHP | 生成的动态调用 |
|---|---|
| `$a + $b` | `operator::add($a, $b)` |
| `$a - $b` | `operator::sub($a, $b)` |
| `$a * $b` | `operator::mul($a, $b)` |
| `$a / $b` | `operator::truediv($a, $b)` |
| `$a % $b` | `operator::mod($a, $b)` |
| `$a ** $b` | `operator::pow($a, $b)` |
| `$a << $b` | `operator::lshift($a, $b)` |
| `$a >> $b` | `operator::rshift($a, $b)` |
| `$a & $b` | `operator::and_($a, $b)` |
| bitwise OR | `operator::or_($a, $b)` |
| `$a ^ $b` | `operator::xor($a, $b)` |
| `-$a` | `operator::neg($a)` |
| `+$a` | `operator::pos($a)` |
| `~$a` | `operator::invert($a)` |
| `$a += $b` | `$a = operator::iadd($a, $b)` |
所有操作数必须严格从左到右求值。
即使源码没有显式写出 `use python\operator`,出现 Python 运算符时,编译器也将其视为一个仅供内部 lowering 使用的隐式 module binding,并通过同一 `pythonModuleMap` 取得 `operator` module。它不向用户文件注入可见别名,因此不会与用户自己定义的 `operator` class 或 use alias 冲突。用户显式 `use python\operator` 时,内部 lowering 和用户访问复用同一个 module ID。
identity 比较调用 `operator::is_()` / `operator::is_not()`。即使两个对象的 `operator::eq()` 结果为真,只要不是同一个 Python object,`===` 仍为假。
允许 Python 对象与 TypePHP 值直接混合运算。只要当前运算节点的一侧静态类型为 `PyObject` 或其子类,另一侧的 TypePHP 表达式先完整地按 TypePHP 规则求值,再把所得值转换为 Python 对象,最后由 CPython 执行当前运算节点对应的 protocol。
例如:
```php
$result1 = $pyInt + 10; // 10 转为 Python int,由 Python 执行加法
$result2 = $pyList * getCount(); // 先求值 getCount(),再转为 Python int
$native = $pyInt->toPlainValue()->toInt() + 10; // 已显式转为 TypePHP int,使用 TypePHP 加法
```
`operator` 调用结果仍为 `PyObject`,以保留 Python 自定义运算符可能返回的任意对象。`===` / `!==` 和条件分支是例外:`operator.is_/is_not/truth()` 的 Python bool 结果随后通过显式 phpy 转换入口得到 TypePHP `bool`。两侧操作数必须严格从左到右各求值一次,转换过程不得导致表达式重复执行。
phpy 作为普通 PHP 扩展时,可以继续使用 Zend opcode handler 提供运算符重载兼容性;TypePHP 不依赖这些 handler。
TypePHP 编译器在识别到静态类型为 `PyObject`、`PyDict` 等 phpy 对象时,把运算符改写为普通 Python module callable 调用:
```text
TypePHP operator
-> compile-time lowering
-> implicit python\operator module binding
-> operator::add/sub/... dynamic call
-> CPython complete operator protocol
```
该抽象不直接链接 phpy,也不经过 phpy 的 user opcode handler,但它不是无调用成本的 C++ inline 操作:
- `zend_function*` 和 class entry 使用现有 func/class map lazy cache。
- 参数仍需要构造为 Zend values,并由 phpy 转为 Python 对象。
- Python module member lookup、GIL、CPython call 和引用计数成本仍然存在。
- 优点是 TypePHP 二进制只依赖 ZendVM/PHPX,phpy 可以作为真正的可选运行时扩展。
使用标准库 `operator.add()` 而不是直接调用 `__add__()`,可以复用 CPython 对 `NotImplemented`、`__radd__()`、右操作数子类优先级等完整规则,TypePHP 不实现 reflected-operation fallback。
当前实现已经覆盖二元算术和位运算、比较、identity、一元运算、条件真假值、短路逻辑,以及 variable、属性和下标左值的复合赋值。Python module function/property、builtin、动态方法、属性、下标和 callable 的结果都会继续传播 `PyObject` 静态类型,因此可以直接链式访问或参与后续 Python 运算。
## 16. 异常
Python 调用失败时抛出统一的 TypePHP 异常类型,暂定:
```php
PyError
```
异常至少保留:
- Python exception type。
- message。
- Python traceback 对象。
- 格式化后的 traceback 字符串。
- 原始 Python exception instance。
示例:
```php
try {
np::array('invalid')->reshape(2, 2);
} catch (PyError $error) {
echo $error->pythonType();
echo $error->pythonTraceback();
}
```
Python 同步调用 TypePHP callable 代理时,如果 TypePHP 抛出异常,应转换为普通 Python 异常,并保留原始 TypePHP 类名和消息。该异常只沿当前动态调用栈传播,不要求注册 `typephp` Python module 或专用的全局异常类型。
异常跨 VM 后必须清理源 VM 的 pending exception 状态。任何异常转换失败都不能导致 coredump、重复抛出或遗留错误状态。
## 17. TypePHP callable 传给 Python
TypePHP 函数、闭包和可调用对象可以自动包装为 Python callable:
```php
$values = python\list([1, 2, 3]);
$result = python\map(fn (int $value): int => $value * 2, $values);
```
Python 调用代理时:
1. Python 参数按边界规则转换或包装为 TypePHP 值。
2. 进入 ZendVM 调用 callable。
3. 返回值转换为 Python 值。
4. TypePHP 异常转换为 Python 异常。
闭包代理必须持有 Zend callable,防止 callable 在 Python 仍引用它时被释放。跨 VM 引用环必须由运行时显式检测或提供可预测的回收策略。
TypePHP callable 代理只是参数值,不是导出机制:只有 TypePHP 主动把代理传给 Python 后,Python 才能在该对象存活期间动态调用它。TypePHP 不生成可供 Python 独立导入的 module,也不注册全局函数或类。
## 18. phpy 生命周期与集成方式
TypePHP 复用 phpy 自己的 PHP 扩展入口和生命周期,不增加独立的 CPython bootstrap:
1. phpy 的 `MINIT` 初始化共享运行时、CPython 以及 `PyObject`、`PyDict` 等 Zend 类。
2. phpy 的 `RINIT` 建立本次请求需要的状态。
3. TypePHP 程序在请求期间通过 phpy 注册到 ZendVM 的 internal classes、methods 和 object handlers 动态调用 Python。
4. phpy 的 `RSHUTDOWN` 释放请求级资源和代理。
5. phpy 的 `MSHUTDOWN` 在所有代理均已安全释放后关闭共享运行时和 CPython。
TypePHP 应通过与其他静态或动态链接 PHP 扩展相同的机制执行这些入口,不能重复初始化 CPython,也不能绕过 phpy 生命周期直接调用 `Py_Initialize()``Py_Finalize()`
唯一产物是以 TypePHP 为入口的主程序或库。不会生成可被 CPython 导入的 `.so` / `.pyd`,不会向 Python 注册 TypePHP module、函数或类,也不存在 `#[PythonExport]`
## 19. 性能原则
- `PyObject` 传参只增加必要的引用计数,不复制 Python 对象。
- `pythonModuleMap` 只缓存已经绑定的 `PyModule` Zend object zval,真实加载和全局 identity 直接复用 CPython `sys.modules`;builtin/member lookup 初版保持简单,只有基准测试证明必要时才单独设计缓存。
- 参数应直接构造 vectorcall 所需数组,优先使用 CPython vectorcall API。
- 避免先构造 PHP 数组,再由 phpy 二次转换为 Python tuple/dict。
- TypePHP 数组到 Python 容器属于显式 O(n) 转换,不宣称零成本。
- 对进入热点 Python 调用的 TypePHP 数组和字符串,应提升为可复用的 `PyList`、`PyDict`、`PyStr`;编译器不擅自缓存转换结果,因为原 TypePHP 值可能已经改变。
- GIL guard 应覆盖最小必要区域;单线程同步重入期间必须保持正确的解释器状态。
- 异常路径与正常路径必须同等测试引用计数和内存泄漏。
## 20. 永久边界与不支持能力
- Python 线程,包括 `threading` 创建线程以及任何从非主线程进入 phpy/TypePHP bridge 的调用。
- `asyncio`、Python coroutine、`async`/`await` 及跨语言事件循环调度。
- CPython subinterpreter 和 per-interpreter GIL 模式。
- Python 作为入口独立加载 TypePHP 程序。
- 生成 Python extension 或将 TypePHP 函数、类、对象注册为可导入的 Python module。
- 运行时反射生成 TypePHP 静态类型。
- 自动导入 `from module import *`
- pickle/serialize Python 对象。
- 跨进程传递 `PyObject`
- WASM target 中的 Python 互调用。
禁止能力必须有明确防线:编译器对能够静态识别的 `threading`、`_thread`、`asyncio` 和 subinterpreter API 给出 FatalError;phpy 记录创建运行时的 owner thread,并拒绝从其他线程进入 ZendVM bridge。动态导入、反射或第三方包不能被编译器完整识别,因此运行时检查不能省略。
第三方 native package 内部完全封闭、从不进入 CPython API 或 phpy/ZendVM bridge 的计算线程不属于这里的 Python 线程能力;它们对 TypePHP 不可见,也不得产生跨线程回调。
## 21. TDD 与测试门禁
本项目的实现和重构必须严格遵循 TDD,顺序不可颠倒:
1. 根据已确认的设计语义编写测试。
2. 运行测试,确认它因为目标能力尚未实现或现有 BUG 而失败。
3. 编写使该测试通过的最小实现。
4. 运行相关测试和完整回归。
5. 在测试保护下重构、清理和优化。
6. 再次运行完整回归、内存检查和覆盖率检查。
禁止先完成实现,再补写只能验证当前实现细节的测试。每个 BUG 必须先添加能够稳定复现问题的回归测试。
### 21.1 三层强制测试
#### PHPUnit
TypePHP 仓库的 PHPUnit 用于验证编译器自身:
- Python import 和特殊名称解析。
- AST、符号表和类型推断。
- C++ 代码生成。
- 编译期错误和诊断位置。
- 永久禁用能力的编译期诊断,以及无 phpy 环境仍能成功生成代码。
- 不需要启动 CPython 的边界逻辑。
phpy 仓库现有 PHPUnit 用于验证 ZendVM/PHP Facade 与共享 Runtime:
- `PyCore`、`PyObject`、`PyDict` 等公开 PHP API。
- PHP 值与 Python 对象转换。
- Python 异常映射为 `PyError`
- opcode handler 与 TypePHP 使用的 Zend dynamic-call API 具有一致语义。
- TypePHP 需要的对象保持型调用路径。
- GIL、引用计数、析构和异常路径。
#### PHPT
用于从 TypePHP 用户视角验证语言和运行时的端到端行为:
- 导入、`module::$name` 包变量读取、`module::name()` callable 调用和关键字参数。
- `use python\module as alias` 与手写 `$alias = PyCore::import('module')` 的结果、异常和对象 identity 等价。
- 多个别名、嵌套模块和跨 `.cc` 重复导入。
- 只有 `use python\module` 而未访问任何别名符号时,不生成 helper、不调用 import,也不检查该 Python module 是否存在。
- 同一完整 module 名称跨函数、跨 `.cc` 只分配一个 ID,并只在首次访问时调用 import API。
- import 失败保持 map slot 为 `UNDEF`;异常被捕获后,下一次访问可以重新尝试。
- request clean 对 module zval 逐项执行 `zval_ptr_dtor()` 并恢复为 `UNDEF`,不得直接 `memset` 有效 Zend object。
- 删除或替换 `sys.modules` 条目不会改变已经完成的 TypePHP module alias binding。
- `module::name` 常量式访问的编译期 FatalError,以及不存在成员和不可调用成员的运行时异常。
- 属性、下标、迭代、运算符和 truthiness。
- TypePHP 参数到 Python 的转换,以及 Python 返回值的显式转换。
- 空 TypePHP 数组默认转换为 Python list,以及数组递归深拷贝、异常中止和重复转换行为。
- Python builtin、模块函数、方法和运算结果不会隐式变成 TypePHP 标量。
- `$obj->toPlainValue()->toInt()`、`python\scalar($obj)->toInt()` 等显式边界及其后的普通 TypePHP 转换恢复静态类型和运算规则。
- Python 异常到 TypePHP 异常。
- phpy 未加载时首次 Python 调用抛出 PHP `Error`,而仅声明未使用的 Python `use` 不报错。
- TypePHP callable 被 Python 回调。
- 引用计数、对象析构和重复调用。
- 编译后的真实程序输出,而不是只检查生成代码字符串。
#### pytest
pytest 用于 phpy 自身已有 Python-facing bridge 的回归测试;它不表示 TypePHP 会生成 Python extension。需要验证:
- Python 调用 PHP 函数、对象和 callable。
- 同步重入和 phpy module 生命周期。
- Python 对 Zend callable/object proxy 的持有、释放和异常映射。
- 永久禁止从 Python 线程进入 ZendVM 的防护。
三层测试不能互相替代。C++/GoogleTest 可以覆盖 phpy 内部的引用计数、RAII 和低层转换,但不能代替 PHPUnit、PHPT 或 pytest。
### 21.2 每项语义的测试矩阵
每个已支持能力至少考虑以下维度:
- 正常路径。
- 错误类型和错误消息。
- 边界值及空值。
- Python 子类和动态协议。
- TypePHP → Python → TypePHP 重入。
- 仅由 TypePHP 发起、经 callable 代理发生的 Python → TypePHP → Python 同步重入。
- 正常析构和异常析构。
- 重复执行、`sys.modules` identity 和重复 import 不重新执行 module 代码。
- Debug、Release 以及支持的平台。
转换测试必须包含:
- `PHP_INT_MIN/PHP_INT_MAX` 及超出范围的 Python int。
- `NaN`、`INF`、`-INF` 和负零。
- 空字符串、Unicode、无效 UTF-8、内含 NUL 的 bytes。
- 空 list/dict、混合 key、深层容器、递归容器和循环引用。
- 同一 Python 对象经多次包装后的 identity。
### 21.3 内存与稳定性测试
涉及 `PyObject*``zval` 所有权的修改,除功能测试外还必须执行:
- PHP memory leak report。
- Python debug build/refcount 检查(环境可用时)。
- ASan/UBSan 构建。
- 异常注入测试,覆盖每一个可能提前返回的分支。
- 循环创建和销毁对象的压力测试。
- 进程退出时仍存在跨 VM 代理对象的测试。
不允许把 coredump、泄漏或未清理的 pending exception 标记为“预期行为”来绕过测试。
### 21.4 覆盖率要求
- 设计文档中每一条规范性行为都必须能够对应到至少一个测试。
- 新增和修改的桥接代码需要覆盖正常分支与错误分支。
- 项目整体覆盖率不得因本功能下降。
- 对 GIL、引用计数、异常和析构代码,不能只依赖行覆盖率,必须人工检查分支矩阵。
- 最终 coding 计划必须先列出测试清单,再列实现任务。
## 22. 已确认与待确认问题
已确认:
1. Python 互调用是可选的扩展级特性;TypePHP 不链接或在编译期检查 `libphpy.so`,首次实际调用时若 phpy 未加载则由 Zend 抛出 PHP `Error`
2. TypePHP 尽可能采用显式转换,不继承 phpy 的全部隐式转换行为。
3. TypePHP 运算符在编译期改写为 `operator::add($left, $right)` 一类 Python 标准库调用,不使用 phpy opcode handler,也不生成 phpy C++ 符号调用。
4. `python` 根命名空间大小写不敏感,其后的所有 Python 符号大小写敏感。
5. `python` 是编译器处理的特殊语言命名空间。
6. 运行时类继续使用 `PyObject`、`PyDict` 等 phpy 公开名称。
7. `python\dict()` 等构造语法是现有 phpy 类构造器的语法糖;`python\print()` 等是 `PyCore` API 的语法糖。
8. `new PyList()``python\list()` 具有相同的 `PyList` typed object 类型和优化能力。
9. phpy 解决运行时问题,TypePHP 只通过缓存的 `zend_function*` 和 PHPX/Zend 通用对象 API 动态调用 phpy Facade。
10. TypePHP 的 Python 专用实现与测试放入独立子目录,通过受控入口接入通用编译流程。
11. Python 线程、`asyncio` 和 subinterpreter 永久禁止,且不作为后续兼容目标。
12. `===` / `!==` 分别映射 Python identity 的 `is` / `is not`;`==` / `!=` 使用 Python 值比较。
13. 仅支持 TypePHP 主动调用 Python;不生成 Python extension,不提供 `#[PythonExport]`,不向 Python 注册 TypePHP 符号。
14. CPython 和 bridge 生命周期完全复用 phpy 的 `MINIT/RINIT/RSHUTDOWN/MSHUTDOWN` 入口。
15. Python 包变量使用 `math::$pi` 形式读取;`math::pi` 保持 PHP 常量访问含义,不被重新解释。
16. `np::array()` 表示读取并调用 Python 包成员;该成员可以是函数、class 或其他 callable,具体类型由 Python 运行时决定。
17. `PyObject` 可以与 TypePHP 值混合运算;TypePHP 操作数转换为 Python 对象后,整个运算由 CPython protocol 执行,结果保持为 `PyObject`
18. Python 函数、方法、class 构造和 builtin 调用的结果一律保持为 `PyObject` 或已知的 phpy 子类;禁用 phpy 返回值隐式转换。
19. `PyObject::toPlainValue()` 是推荐的链式显式转换关键词;`python\scalar()` 保留为等价入口。两者退出 Python 类型规则后均可继续使用普通 TypePHP 转换,例如 `$obj->toPlainValue()->toInt()`
20. TypePHP 调用 Python 时,所有参数自动转换为 Python 类型;TypePHP 数组递归深拷贝,空数组默认转换为 Python list。
21. 性能敏感代码应复用 `PyDict`、`PyList`、`PyStr` 等代理对象,避免同一 TypePHP 值反复转换和深拷贝。
22. TypePHP 的主要语言增量是 `use python\...` 和模块别名;使用别名时通过与 `funcMap` 同类的 lazy indexed map 调用 phpy import,其他运行时能力优先直接复用 phpy。
23. `use python\module` 只登记 namespace 标记;当前 `.php` 文件没有使用该别名的任何符号时,不生成 helper,也不执行运行时 import。
24. 发现 `module::$attr``module::func()` 时,才为完整 module 名称分配 ID;未使用的 `use` 不占 map slot,也不执行 import。
25. `pythonModuleMap``funcMap` 一样集中声明、按 ID lazy lookup;区别是 module 保存为拥有引用的 Zend object zval,必须在 request clean 中逐项 `zval_ptr_dtor()` 并恢复为 `UNDEF`
26. `sys.modules` 负责全局加载状态和 identity,`pythonModuleMap` 只表示 TypePHP 已经完成的 module alias binding。
27. TypePHP 生成代码只依赖 PHPX/ZendVM;`PyCore::import()`、builtin、对象方法和转换均解析为 `zend_function*` 动态调用。
28. Python 运算符隐式使用 `python\operator` module;完整运算协议由 CPython `operator` 函数处理,不直接调用 dunder,也不由 TypePHP 实现 reflected fallback。
仍待确认:
1. 初版是否需要模块属性赋值?如果需要,是否使用 `module::$name = $value`
该待确认项不阻塞只读 module binding 等已确认阶段的实施;属性写入必须在语义确认并先补测试后才能实现。

@ -0,0 +1,91 @@
# TypePHP Python 互调用分阶段实施计划
> 本计划以 `python/design.md` 为规范。每个阶段严格执行:先增加 PHPUnit/PHPT/pytest 测试并确认失败,再实现,再运行相关测试和完整回归。
## 阶段 1:Python use 与 module binding
目标是完成最小可运行闭环,不实现运算符和通用转换:
1. 识别 `use python\module`、根名称大小写不敏感和 Python 后续名称大小写敏感。
2. 建立文件级 Python module alias 表,并与普通 class/function/constant use 检查冲突。
3. 仅在出现 `module::$attr``module::func()` 时分配 module ID。
4. 生成与 `funcMap` 同类的 `pythonModuleMap`、lazy getter 和 request-clean 代码。
5. 使用 Zend class/function map 动态调用 `PyCore::import()`;不 include、link 或检测 phpy。
6. 使用 Zend object API 读取 module 属性及调用 module callable。
7. phpy 未加载时在首次实际使用处抛出 PHP `Error`;未使用的 Python use 不触发错误。
测试顺序:PHPUnit 代码生成与诊断测试 → PHPT 运行时测试 → 现有 compiler 回归。
## 阶段 2:builtins、构造语法糖与静态类型
1. `python\name()` 通过 phpy Zend Facade 动态调用:显式 `PyCore` 方法直接复用,其他名称经 Python `builtins` module lookup。
2. `python\list/dict/tuple/set/str/object()` 映射既有 phpy Zend 类或方法。
3. `new PyList()``python\list()` 等写法获得相同的逻辑静态类型。
4. Python 调用结果保持 `PyObject` 或已知 phpy 子类,关闭 TypePHP 路径的隐式 scalar conversion。
5. 缺少 phpy、builtin 不存在、参数错误和异常映射测试。
实现状态:已完成。TypePHP 在首次实际执行 Python 表达式时延迟启用 phpy 的 `return_as_object`,仅声明未使用的 Python 符号仍不触发运行时依赖。
## 阶段 3:参数转换与显式结果转换
1. TypePHP 参数从左到右求值后自动转换为 Python 值。
2. 标量、数组、空数组、嵌套容器及 TypePHP callable 转换。
3. 通过 `$py->toPlainValue()` 或兼容入口 `python\scalar($py)` 离开 Python 对象规则;需要确定原生类型时继续使用普通 TypePHP 转换,例如 `$py->toPlainValue()->toInt()`
4. 深拷贝、递归容器、溢出、Unicode/bytes 和异常路径测试。
5. review 并重构 phpy 转换策略,移除影响同步重入的全局临时转换状态。
实现状态:核心边界已完成。TypePHP 参数严格从左到右求值,支持标量、空数组、嵌套 list/dict 与 callable;`toPlainValue()` 是推荐的链式显式转换入口,`python\scalar()` 保留为等价兼容入口。PHPX 在运行时确认 `php::Var` 确实持有 `PyObject`,再通过 Zend Facade 调用 `PyCore::scalar()`;不依赖 phpy C++ 符号。phpy 已移除进程级转换函数指针,改为局部有状态转换器、RAII 递归保护和 128 层深度限制,并覆盖无效 UTF-8、PHP 自引用数组及 Python 循环容器错误路径。Python 大整数与 bytes 的最终语言映射仍保留在本阶段后续工作中。
## 阶段 4:运算符
1. 将运算符改写为 Python 标准库 `operator` module 的动态调用。
2. 混合操作数先转换为 `PyObject`
3. 严格保证从左到右、各求值一次。
4. 使用 `operator.is_/is_not/truth` 实现 identity 和 truthiness,使用 `iadd/isub/...` 实现 compound assignment。
5. 验证 `operator` 自动处理 `NotImplemented`、reflected dunder 和子类优先级。
6. 对照 phpy opcode-handler 行为,修复 `/` 错误映射 floor division 等既有问题。
实现状态:已完成。二元算术、位运算、比较、`===`/`!==`、一元运算、条件真假值、短路逻辑和复合赋值均通过隐式 `operator` module binding 执行;`/` 使用 `truediv`。混合 TypePHP 操作数由 phpy 在调用边界转换,结果继续保持 `PyObject`,比较和真假值结果显式收敛为 TypePHP `bool`。属性和下标左值由阶段 5 的动态写入协议完成回写。
## 阶段 5:完整对象协议
1. Python 对象属性读写和删除。
2. 下标读写、删除、`isset()`。
3. iterator/foreach。
4. Python callable 和 TypePHP callable proxy 的同步重入。
5. keyword argument、argument unpacking 和错误语义。
实现状态:已完成。Python proxy 的动态属性、未知方法、下标、删除、`isset()`、`foreach` 和 callable 均复用 phpy 的 Zend object protocol,不生成 phpy C++ 符号。方法、属性、下标和 callable 结果会继续传播为 `PyObject`,支持链式访问和后续 Python 运算。named argument 与 unpacking 复用统一调用参数管线并保持从左到右求值;属性和下标复合赋值使用 `operator.i*()` 的返回对象回写原左值。
phpy 同步完成了对象协议加固:`__set()` 转换引用释放、`__unset()`、list/tuple 负索引、list 删除、缺失键与 Python `None``isset()` 语义、删除和 contains 状态检查,以及 iterator/count 异常传播。相关 BUG 均由 phpy PHPUnit 与 TypePHP PHPT 独立覆盖。
## 阶段 6:phpy 稳定性与性能收尾
1. CPython/ZendVM 生命周期、GIL、owned/borrowed/stolen reference 全量审计。
2. Python/Zend 异常状态和 traceback 审计。
3. 跨 VM 引用环、析构和异常注入测试。
4. ASan/UBSan、PHP leak report、Python debug build 和压力测试。
5. 基准测试动态 Zend call、module map、参数转换和 `operator` module 调用;只优化被数据证明的热点。
6. 完整 PHPUnit、pytest、PHPT 和现有 TypePHP compiler 回归。
实现状态:进行中。第一轮 CPython 失败路径审计已覆盖通用对象、list、dict、tuple、set 的构造和下标写入,以及 sequence/set 的 `contains()`。PHP 到 Python 的 key/value 转换失败现在会立即映射为 `PyError`,所有已取得的新引用均由作用域守卫释放;构造失败不再留下未处理的 CPython error indicator,`contains()` 的 `-1` 错误结果也不再被误判为 `true`。无效 UTF-8、unhashable set member、失败后容器仍可继续使用等路径已有 phpy PHPUnit 回归测试。
第二轮审计覆盖 module import、异常转换、callable 检查和显式 iterator API。`PyImport_ImportModule()`、`PyErr_Fetch()` 和 `PyIter_Next()` 转移给调用方的新引用现在都会在 Zend wrapper 取得独立引用后统一释放;重复 import、Python 异常或显式 iterator next 不再持续增加引用计数。调用非 callable 的 Python 属性或 `PyObject` 会稳定抛出 `PyError(TypeError)`,不再因为 `PyCallable_Check()` 未设置 error indicator 而静默返回 `null`。`PyCore::next()` 也会区分正常迭代结束和 iterator 异常。上述路径均先建立失败的 phpy PHPUnit 回归测试,其中对象调用行为另有 TypePHP PHPT 集成覆盖。
第三轮审计覆盖 `PyCore` Facade 的转换失败与函数缓存。`PyCore::eval()` 会在 globals 转换失败后立即抛出 `PyError`,`PyCore::bytes()` 对非字符串标量使用转换后的 `zend_string`,两者不再解引用空指针或错误的 zval union 字段而导致进程崩溃。`PyCore::next()` 同时释放参数转换产生的 iterator 引用。builtin/operator 函数缓存改用 `std::string` 内容键,不再把请求级 `char*` 地址作为长期 key,也避免同名动态调用不断重复缓存并增加 Python function 引用计数;调用存在但不可调用的 builtin 会释放临时引用并抛出 `PyError(TypeError)`。所有问题均由先失败的独立 PHPUnit 覆盖,其中两个崩溃用禁用 core dump 的隔离进程确认退出码 139 后再修复。
第四轮审计覆盖 Python 到 PHP 的同步回调边界。phpy 会将 Python keyword arguments 转换为 Zend named parameters,并在任一位置参数或命名参数转换失败后立即停止,不会执行只接收到部分参数的 PHP callable。PHPX 为 AOT 原生闭包生成并管理 Zend `arg_info` 参数名元数据,因此 Python kwargs 可以按名称绑定到 TypePHP 闭包,而不是依赖参数位置或降级为字符串 callable。phpy PHPUnit、PHPX 单元测试和 TypePHP PHPT 分别覆盖了转换失败、Zend 命名绑定以及完整的 Python→TypePHP 回调链路。
第五轮审计覆盖 Python 字符串跨 Zend 边界时的异常和所有权。Python 孤立代理字符无法编码为 UTF-8 时,`phpy.String`、动态 PHP 类名、字典键、`PyObject::__toString()` 和 Python 异常消息格式化都不会再使用空指针或未初始化长度;修复前相关隔离测试会退出 139 或尝试分配异常大的内存。`StrObject` 现在具有显式有效状态,所有调用方必须在访问指针前检查转换结果;异常消息的字符串化仅作为 best-effort 辅助信息,失败时保留原始 Python error/type/value,并清理临时 CPython error indicator。`new_string()` 同时补齐 Zend carrier 析构注册,定长字符串直接取得唯一的 `zend_string` 引用,消除了成功路径的泄漏和未初始化 zval。
第六轮引用审计修复了 `PySequence::slice()` 的 new-reference 泄漏。切片在包装为 Zend `PyObject` 后会释放 CPython API 返回的原始所有权,同时保留 wrapper 自己持有的引用;由 `sys.getrefcount()` 压力测试验证重复创建并销毁切片不会继续增加元素引用计数。切片创建失败也会在接触空指针前转换为 `PyError`
内存门禁使用 Valgrind Memcheck 执行。测试关闭 Zend allocator 与 PCRE JIT,在最小独立进程中分别循环 100 次 PHP Closure kwargs 回调、可调用 PHP 对象 kwargs 回调、sequence slice 创建销毁、无效 Unicode 的对象字符串化、字典键转换和异常格式化。结果为 0 invalid-access、0 definite leak、0 indirect leak;进程退出时由 PHP/CPython 保留的 493,106 bytes 均为 still-reachable,不计为泄漏。ASan 扩展无法安全 `dlopen` 到当前启用了 `RTLD_DEEPBIND` 的非 ASan PHP,因此本轮采用不要求 PHP 同步重编译的 Valgrind 作为内存检查工具。
## 阶段门禁
- 当前阶段的失败测试未先建立,不开始实现。
- 当前阶段所有测试未通过,不进入下一阶段。
- phpy 的行为变更必须先在 phpy 仓库增加 PHPUnit/pytest 测试。
- 每个已修复 BUG 必须保留独立回归测试。
- 不以修改第三方测试期望来掩盖实现差异。

@ -69,6 +69,7 @@ use TypePhp\Platform\Macos;
use TypePhp\Platform\PlatformBase;
use TypePhp\Platform\PlatformFactory;
use TypePhp\Platform\Windows;
use TypePhp\Python\PythonModuleTrait;
use TypePhp\Resolver\DeclarationSymbolTrait;
use TypePhp\Resolver\MagicMethodDetector;
use TypePhp\Resolver\PropertyAccessContext;
@ -101,6 +102,7 @@ class CompilerBase implements PropertyAccessContext
use CompilationStateTrait;
use NativeTypeCompatibilityTrait;
use NativeBuildConfigurationTrait;
use PythonModuleTrait;
use DeclarationSymbolTrait;
use NameResolutionTrait;
use AstNodeType;
@ -154,18 +156,24 @@ class CompilerBase implements PropertyAccessContext
* Use findKeywordMethod() for unified lookup including keyword extension methods.
*/
public const array KEYWORD_METHOD_MAP = [
'toInt' => Type::INT,
'toFloat' => Type::FLOAT,
'toString' => Type::STR,
'toBool' => Type::BOOL,
'toArray' => Type::ARRAY,
'toStream' => Type::STREAM,
'toBigInt' => Type::BIGINT,
'toBigFloat' => Type::BIGFLOAT,
'toDecimal' => Type::DECIMAL,
'toObject' => Type::OBJECT,
'toAny' => Type::VAR,
'toRef' => Type::REF,
'toInt' => Type::INT,
'toFloat' => Type::FLOAT,
'toString' => Type::STR,
'toBool' => Type::BOOL,
'toArray' => Type::ARRAY,
'toStream' => Type::STREAM,
'toBigInt' => Type::BIGINT,
'toBigFloat' => Type::BIGFLOAT,
'toDecimal' => Type::DECIMAL,
'toObject' => Type::OBJECT,
'toAny' => Type::VAR,
'toPlainValue' => Type::VAR,
'toRef' => Type::REF,
];
/** Keyword methods not listed here accept no arguments. */
public const array KEYWORD_METHOD_WITH_ARGUMENTS = [
'toObject' => true,
];
private const array STREAM_FUNCTIONS = [
@ -1046,6 +1054,7 @@ class CompilerBase implements PropertyAccessContext
$this->useAliases = [];
$this->useFunctions = [];
$this->useConstants = [];
$this->resetPythonModuleAliases();
$this->namespace = '';
}
@ -1774,6 +1783,21 @@ class CompilerBase implements PropertyAccessContext
protected function detectClassOfExpr(NodeAbstract $expr): string
{
if ($expr instanceof Expr\MethodCall && $this->isNamedMethod($expr->name)) {
$keywordType = $this->findKeywordMethod($this->parseIdentifier($expr->name));
if ($keywordType !== null && $keywordType !== Type::OBJECT) {
return '';
}
}
$pythonOperatorClass = $this->detectPythonOperatorReturnClass($expr);
if ($pythonOperatorClass !== null) {
return $pythonOperatorClass;
}
$pythonClass = $this->detectPythonExpressionReturnClass($expr);
if ($pythonClass !== null) {
return $pythonClass;
}
if ($this->isNewExpr($expr) and $this->isNameExpr($expr->class)) {
$class = $this->parseIdentifier($expr->class);
if ($class === 'self') {
@ -2553,6 +2577,21 @@ class CompilerBase implements PropertyAccessContext
protected function detectTypeOfExpr($expr): string
{
if ($expr instanceof Expr\MethodCall && $this->isNamedMethod($expr->name)) {
$keywordType = $this->findKeywordMethod($this->parseIdentifier($expr->name));
if ($keywordType !== null) {
return $keywordType;
}
}
$pythonOperatorType = $this->detectPythonOperatorReturnType($expr);
if ($pythonOperatorType !== null) {
return $pythonOperatorType;
}
$pythonType = $this->detectPythonExpressionReturnType($expr);
if ($pythonType !== null) {
return $pythonType;
}
$exprType = $expr->getType();
switch ($exprType) {
case 'Expr_UnaryMinus':
@ -2709,11 +2748,6 @@ class CompilerBase implements PropertyAccessContext
case 'Expr_MethodCall':
if ($this->isNamedMethod($expr->name)) {
$method = $this->parseIdentifier($expr->name);
// keyword methods (to* builtins + __ extensions) — return type is known regardless of receiver
$kwType = $this->findKeywordMethod($method);
if ($kwType !== null) {
return $kwType;
}
// Class definition resolution (handles this_, typed VarExpr)
$classDef = $this->resolveObjectClassDef($expr->var);
if ($classDef !== null && $classDef->hasMethod($method)) {

@ -23,7 +23,7 @@ use PhpParser\Node\Expr\Variable;
trait ClosureGenerator
{
protected function genNewClosure(string $callback, string $uses, bool $hasThis): string
protected function genNewClosure(string $callback, string $uses, bool $hasThis, array $params = []): string
{
$thisArg = $hasThis ? 'this_' : '{}';
if ($this->classDef?->trait !== null) {
@ -36,7 +36,15 @@ trait ClosureGenerator
? $this->getClassEntryPtr($this->getFullClassName())
: 'nullptr';
}
return 'php::newClosure(' . $callback . ', ' . $uses . ', ' . $thisArg . ', ' . $scope . ')';
$parameterNames = [];
foreach ($params as $param) {
$name = is_string($param->var->name)
? $param->var->name
: $this->unescapeVarName($this->parseIdentifier($param->var));
$parameterNames[] = $this->genCharPtr($name, true);
}
return 'php::newClosure(' . $callback . ', ' . $uses . ', ' . $thisArg . ', ' . $scope
. ', { ' . implode(', ', $parameterNames) . ' })';
}
protected function parseArrowFunction(Expr\ArrowFunction $expr): string
@ -233,7 +241,8 @@ trait ClosureGenerator
return $this->genNewClosure(
$tmpVar,
'{ ' . implode(', ', $useVars) . ' }',
$this->methodDef !== null
$this->methodDef !== null,
$params
);
}
@ -347,7 +356,7 @@ trait ClosureGenerator
$code .= $this->getIndent() . '};' . PHP_EOL;
$args = $capturedArgs ? '{ ' . implode(', ', $capturedArgs) . ' }' : '{}';
$callback = $this->genNewClosure($callbackVar, $args, $this->methodDef !== null);
$callback = $this->genNewClosure($callbackVar, $args, $this->methodDef !== null, $params);
$code .= $this->getIndent() . 'return typephp_new_fiber_generator(' . $callback . ');' . PHP_EOL;
return $code;
}

@ -216,6 +216,7 @@ trait AssignOpTrait
$type = $this->detectTypeOfExpr($right);
$finalVarType = $this->getNormalAssignType($type);
$runtimeObjectAssignClass = '';
$assigningNullToTypedObject = false;
if ($type === Type::VOID) {
$type = Type::VAR;
}
@ -236,12 +237,9 @@ trait AssignOpTrait
if ($var === 'this_') {
$this->fatalError($left, 'Cannot re-assign $this');
}
if ($this->hasVar($var)
$assigningNullToTypedObject = $this->hasVar($var)
&& $this->getVarType($var) === Type::OBJECT
&& $this->isNull($right)) {
$class = $this->getDeclaredObjectType($var) ?: 'object';
$this->fatalError($right, "Cannot assign null to typed object `\${$var}` of type `{$class}`; use unset() to clear it");
}
&& $this->isNull($right);
if ($this->isStdContainer($var)) {
$copyAssign = $this->parseStdContainerCopyAssign($var, $right);
if ($copyAssign !== null) {
@ -343,7 +341,10 @@ trait AssignOpTrait
$finalVarType = $this->getVarType($var);
$this->checkVarAssignExpr($left, $finalVarType, $type);
$declaredObjectClass = $this->getDeclaredObjectType($var);
if ($finalVarType === Type::OBJECT && $declaredObjectClass !== '' && ($type === Type::VAR || $type === Type::OBJECT)) {
if (!$assigningNullToTypedObject
&& $finalVarType === Type::OBJECT
&& $declaredObjectClass !== ''
&& ($type === Type::VAR || $type === Type::OBJECT)) {
$runtimeObjectAssignClass = $declaredObjectClass;
}
}
@ -372,8 +373,20 @@ trait AssignOpTrait
if ($propertyWriteTarget !== null) {
$rightExpr = $this->wrapPropertyWriteTypeCheck($propertyWriteTarget, $right, $rightExpr);
}
if ($assigningNullToTypedObject) {
// php::Object intentionally keeps the inferred class as its C++ type,
// while an empty/null value represents the same state as unset($var).
$rightExpr = 'php::Object{' . $rightExpr . '}';
}
if ($runtimeObjectAssignClass !== '') {
$rightExpr = 'php::toObject(' . $rightExpr . ', ' . $this->getClassEntryPtr($runtimeObjectAssignClass) . ')';
// A typed object has two valid runtime states: the declared class or
// null. Evaluate a dynamic RHS once, preserve null, and validate only
// actual objects against the inferred class constraint.
$checkedValue = 'typephp_nullable_object_value';
$rightExpr = '([&](php::Var ' . $checkedValue . ') -> php::Object {'
. ' if (' . $checkedValue . '.isNull()) { return php::Object{' . $checkedValue . '}; }'
. ' return php::toObject(' . $checkedValue . ', ' . $this->getClassEntryPtr($runtimeObjectAssignClass) . ');'
. ' })(' . $rightExpr . ')';
}
$leftExprType = $this->detectTypeOfExpr($left);
$rightExprType = $this->detectTypeOfExpr($right);
@ -463,6 +476,10 @@ trait AssignOpTrait
protected function parseAssignOp(Expr\AssignOp $node, string $op): string
{
$this->assertNotNullsafeWriteContext($node->var);
$pythonOperator = $this->parsePythonAssignOperator($node);
if ($pythonOperator !== null) {
return $pythonOperator;
}
$propertyWriteTarget = $this->preparePropertyWriteTarget($node->var);
$this->guardLiteralDivisionByZero($node->expr, $op);

@ -594,12 +594,14 @@ trait BinaryOpTrait
protected function parseBinaryOpPlus(Expr\BinaryOp\Plus $expr): string
{
return $this->parseBinaryOp($expr->left, $expr->right, '+');
return $this->parsePythonBinaryOperator($expr)
?? $this->parseBinaryOp($expr->left, $expr->right, '+');
}
protected function parseBinaryOpMul(Expr\BinaryOp\Mul $expr): string
{
return $this->parseBinaryOp($expr->left, $expr->right, '*');
return $this->parsePythonBinaryOperator($expr)
?? $this->parseBinaryOp($expr->left, $expr->right, '*');
}
protected function parseBinaryOpConcat(Expr\BinaryOp\Concat $expr): string
@ -657,31 +659,40 @@ trait BinaryOpTrait
protected function parseBinaryOpSmaller(Expr\BinaryOp\Smaller $expr): string
{
return $this->convertBoolExpr($this->parseBinaryOp($expr->left, $expr->right, '<'));
return $this->parsePythonBinaryOperator($expr)
?? $this->convertBoolExpr($this->parseBinaryOp($expr->left, $expr->right, '<'));
}
protected function parseBinaryOpShiftLeft(Expr\BinaryOp\ShiftLeft $expr): string
{
return $this->parseBinaryOp($expr->left, $expr->right, '<<');
return $this->parsePythonBinaryOperator($expr)
?? $this->parseBinaryOp($expr->left, $expr->right, '<<');
}
protected function parseBinaryOpShiftRight(Expr\BinaryOp\ShiftRight $expr): string
{
return $this->parseBinaryOp($expr->left, $expr->right, '>>');
return $this->parsePythonBinaryOperator($expr)
?? $this->parseBinaryOp($expr->left, $expr->right, '>>');
}
protected function parseBinaryOpMod(Expr\BinaryOp\Mod $expr): string
{
return $this->parseBinaryOp($expr->left, $expr->right, '%');
return $this->parsePythonBinaryOperator($expr)
?? $this->parseBinaryOp($expr->left, $expr->right, '%');
}
protected function parseBinaryOpGreater(Expr\BinaryOp\Greater $expr): string
{
return $this->convertBoolExpr($this->parseBinaryOp($expr->left, $expr->right, '>'));
return $this->parsePythonBinaryOperator($expr)
?? $this->convertBoolExpr($this->parseBinaryOp($expr->left, $expr->right, '>'));
}
protected function parseBinaryOpPow(Expr\BinaryOp\Pow $expr): string
{
$pythonOperator = $this->parsePythonBinaryOperator($expr);
if ($pythonOperator !== null) {
return $pythonOperator;
}
$this->assertExprCanBeUsedAsValue($expr->left, 'binary operand');
$this->assertExprCanBeUsedAsValue($expr->right, 'binary operand');
$leftType = $this->detectTypeOfExpr($expr->left);
@ -708,17 +719,20 @@ trait BinaryOpTrait
protected function parseBinaryOpBitwiseAnd(Expr\BinaryOp\BitwiseAnd $expr): string
{
return $this->parseBinaryOp($expr->left, $expr->right, '&');
return $this->parsePythonBinaryOperator($expr)
?? $this->parseBinaryOp($expr->left, $expr->right, '&');
}
protected function parseBinaryOpBitwiseOr(Expr\BinaryOp\BitwiseOr $expr): string
{
return $this->parseBinaryOp($expr->left, $expr->right, '|');
return $this->parsePythonBinaryOperator($expr)
?? $this->parseBinaryOp($expr->left, $expr->right, '|');
}
protected function parseBinaryOpBitwiseXor(Expr\BinaryOp\BitwiseXor $expr): string
{
return $this->parseBinaryOp($expr->left, $expr->right, '^');
return $this->parsePythonBinaryOperator($expr)
?? $this->parseBinaryOp($expr->left, $expr->right, '^');
}
protected function parseCompareExpr(NodeAbstract $expr): string
@ -733,18 +747,30 @@ trait BinaryOpTrait
protected function parseBinaryOpEqual(Expr\BinaryOp\Equal $expr): string
{
$pythonOperator = $this->parsePythonBinaryOperator($expr);
if ($pythonOperator !== null) {
return $pythonOperator;
}
return $this->genBigNumericCmp($expr, ' == 0')
?? 'php::equals(' . $this->parseCompareExpr($expr->left) . ', ' . $this->parseCompareExpr($expr->right) . ')';
}
protected function parseBinaryOpNotEqual(Expr\BinaryOp\NotEqual $expr): string
{
$pythonOperator = $this->parsePythonBinaryOperator($expr);
if ($pythonOperator !== null) {
return $pythonOperator;
}
return $this->genBigNumericCmp($expr, ' != 0')
?? '!php::equals(' . $this->parseCompareExpr($expr->left) . ', ' . $this->parseCompareExpr($expr->right) . ')';
}
protected function parseBinaryOpIdentical(Expr\BinaryOp $expr): string
{
$pythonOperator = $this->parsePythonBinaryOperator($expr);
if ($pythonOperator !== null) {
return $pythonOperator;
}
$left = $this->parseCompareExpr($expr->left);
$right = $this->parseCompareExpr($expr->right);
if ($right === 'nullptr') {
@ -827,8 +853,10 @@ trait BinaryOpTrait
$this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $rightAfterStmtCount);
$this->checkVarMustExist($right, $rightExpr);
$leftBool = $this->convertBoolExpr((string) $leftExpr, $this->detectTypeOfExpr($left));
$rightBool = $this->convertBoolExpr((string) $rightExpr, $this->detectTypeOfExpr($right));
$leftBool = $this->convertPythonObjectToBool($left, (string) $leftExpr)
?? $this->convertBoolExpr((string) $leftExpr, $this->detectTypeOfExpr($left));
$rightBool = $this->convertPythonObjectToBool($right, (string) $rightExpr)
?? $this->convertBoolExpr((string) $rightExpr, $this->detectTypeOfExpr($right));
if (!$rightBeforeStmts && !$rightAfterStmts) {
return '(' . $leftBool . ' ' . $op . ' ' . $rightBool . ')';
}
@ -844,7 +872,8 @@ trait BinaryOpTrait
$code .= $this->getIndent() . $rightTmpVar . ' = ' . $rightExpr . ';';
$code .= $this->formatCapturedStmtLines($rightAfterStmts);
$rightExpr = $rightTmpVar;
$rightBool = $this->convertBoolExpr($rightExpr, $this->detectTypeOfExpr($right));
$rightBool = $this->convertPythonObjectToBool($right, $rightExpr)
?? $this->convertBoolExpr($rightExpr, $this->detectTypeOfExpr($right));
}
$code .= $this->getIndent() . 'return ' . $rightBool . ';';
$code .= $this->getIndent() . '}';
@ -860,19 +889,23 @@ trait BinaryOpTrait
$this->assertExprCanBeUsedAsCondition($expr->right, 'logical operand');
$left = $this->parseOrderedBinaryOperand($expr->left);
$right = $this->parseOrderedBinaryOperand($expr->right);
$leftBool = $this->convertBoolExpr($left, $this->detectTypeOfExpr($expr->left));
$rightBool = $this->convertBoolExpr($right, $this->detectTypeOfExpr($expr->right));
$leftBool = $this->convertPythonObjectToBool($expr->left, $left)
?? $this->convertBoolExpr($left, $this->detectTypeOfExpr($expr->left));
$rightBool = $this->convertPythonObjectToBool($expr->right, $right)
?? $this->convertBoolExpr($right, $this->detectTypeOfExpr($expr->right));
return '(' . $leftBool . ' != ' . $rightBool . ')';
}
protected function parseBinaryOpSmallerOrEqual(Expr\BinaryOp\SmallerOrEqual $expr): string
{
return $this->convertBoolExpr($this->parseBinaryOp($expr->left, $expr->right, '<='));
return $this->parsePythonBinaryOperator($expr)
?? $this->convertBoolExpr($this->parseBinaryOp($expr->left, $expr->right, '<='));
}
protected function parseBinaryOpGreaterOrEqual(Expr\BinaryOp\GreaterOrEqual $expr): string
{
return $this->convertBoolExpr($this->parseBinaryOp($expr->left, $expr->right, '>='));
return $this->parsePythonBinaryOperator($expr)
?? $this->convertBoolExpr($this->parseBinaryOp($expr->left, $expr->right, '>='));
}
protected function parseBinaryOpSpaceship(Expr\BinaryOp\Spaceship $expr): string
@ -939,12 +972,14 @@ trait BinaryOpTrait
protected function parseBinaryOpNotIdentical(Expr\BinaryOp $expr): string
{
return '!(' . $this->parseBinaryOpIdentical($expr) . ')';
return $this->parsePythonBinaryOperator($expr)
?? '!(' . $this->parseBinaryOpIdentical($expr) . ')';
}
protected function parseBinaryOpDiv(Expr\BinaryOp\Div $expr): string
{
return $this->parseBinaryOp($expr->left, $expr->right, '/');
return $this->parsePythonBinaryOperator($expr)
?? $this->parseBinaryOp($expr->left, $expr->right, '/');
}
protected function guardLiteralDivisionByZero(NodeAbstract $right, string $op): void
@ -956,7 +991,8 @@ trait BinaryOpTrait
protected function parseBinaryOpMinus(Expr\BinaryOp\Minus $expr): string
{
return $this->parseBinaryOp($expr->left, $expr->right, '-');
return $this->parsePythonBinaryOperator($expr)
?? $this->parseBinaryOp($expr->left, $expr->right, '-');
}
}

@ -17,6 +17,8 @@ trait ClassConstantFetchTrait
{
protected function parseClassConstFetch(Expr\ClassConstFetch $expr): string
{
$this->rejectPythonModuleClassConstantFetch($expr);
if (!$this->isNameExpr($expr->class)) {
return $this->parseDynamicClassConstFetch($expr);
}
@ -24,17 +26,14 @@ trait ClassConstantFetchTrait
$class = $this->parseIdentifier($expr->class);
$self = false;
if ($class === 'self' or $class === 'this_') {
// Trait 读取常量,必须动态获取类名
if ($this->classDef->trait) {
$class = 'static';
} else {
$self = true;
// Trait-composed methods are parsed under the trait's lexical
// namespace, while `self` still denotes the consuming class.
// Keep it fully qualified so the lexical namespace is not
// applied to the class identity below.
$class = '\\' . $this->getFullClassName();
}
$self = true;
// Trait methods are compiled only after their AST is composed into
// the consuming class. During trait preprocessing, however, class
// constant initializers still belong to the trait itself. Resolve
// `self` lexically in both cases; rewriting it to `static` would
// incorrectly require a method scope for expressions such as
// `const B = [...self::A]`.
$class = '\\' . $this->getFullClassName();
} elseif ($class === 'parent') {
if (!$this->classDef || !$this->classDef->extends) {
$this->fatalError($expr, 'Cannot use "parent" outside a class or class does not extend any class');

@ -71,6 +71,11 @@ trait FunctionCallTrait
protected function parseFuncCall(Expr\FuncCall $expr): string
{
$pythonCall = $this->parsePythonBuiltinCall($expr);
if ($pythonCall !== null) {
return $pythonCall;
}
if ($this->isVarExpr($expr->name)) {
$fn = $this->parseIdentifier($expr->name);
$placeHolder = $fn;

@ -332,24 +332,24 @@ trait MethodCallTrait
$magicMethod = false;
$method = $this->identifierToStr($expr->name, literal: true);
// keyword methods (to* builtins + __ extensions) — dispatched before type-specific logic
// Keyword methods are dispatched before all receiver-specific logic.
if ($this->isNamedMethod($expr->name)) {
$methodName = $expr->name->toString();
$receiverType = $this->isVarExpr($expr->var) ? $this->getVarType($object) : $this->detectTypeOfExpr($expr->var);
if ($receiverType === Type::VOID) {
$receiverType = Type::VAR;
}
// to* builtins
if (isset(self::KEYWORD_METHOD_MAP[$methodName])) {
$keywordType = $this->findKeywordMethod($methodName);
if ($keywordType !== null && isset(self::KEYWORD_METHOD_MAP[$methodName])) {
if (!isset(self::KEYWORD_METHOD_WITH_ARGUMENTS[$methodName]) && $expr->args !== []) {
$this->fatalError($expr, "The {$methodName} method does not accept parameters");
}
if ($methodName === 'toObject') {
return $this->genToObjectCall($expr, $object);
}
if ($methodName === 'toRef') {
return $this->genToRefCall($expr);
}
if ($methodName === 'toAny' && !empty($expr->args)) {
$this->fatalError($expr, 'The toAny method does not accept parameters');
}
return $this->genToConvertCall($object, $methodName, $receiverType);
}
// MethodsFor('*') extensions apply to every receiver type.
@ -413,6 +413,9 @@ trait MethodCallTrait
$magicMethod = true;
}
if (!$nativeFunc) {
if ($this->isPythonDynamicMethodCall($expr->var, $methodName)) {
$magicMethod = true;
}
$extension = $this->findObjectExtensionMethod(
$class,
$methodName,

@ -355,6 +355,11 @@ trait PropertyAccessTrait
protected function parseStaticPropertyFetch(Expr\StaticPropertyFetch $expr): string
{
$pythonProperty = $this->parsePythonModuleStaticPropertyFetch($expr);
if ($pythonProperty !== null) {
return $pythonProperty;
}
$native = $this->parseNativeStaticPropertyFetch($expr);
if ($native !== null) {
return $native;

@ -180,6 +180,10 @@ trait TypeConversionTrait
protected function convertConditionExpr(NodeAbstract $node, string $expr): string
{
$pythonBool = $this->convertPythonObjectToBool($node, $expr);
if ($pythonBool !== null) {
return $pythonBool;
}
$type = $this->detectTypeOfExpr($node);
return $this->convertBoolExpr($expr, $type);
}

@ -16,6 +16,10 @@ trait UnaryExpressionTrait
{
protected function parseBitwiseNot(Expr\BitwiseNot $expr): string
{
$pythonOperator = $this->parsePythonUnaryOperator($expr);
if ($pythonOperator !== null) {
return $pythonOperator;
}
$type = $this->detectTypeOfExpr($expr->expr);
$this->assertExprCanBeUsedAsValue($expr->expr, 'bitwise operand');
if ($type === Type::BIGINT) {
@ -27,6 +31,10 @@ trait UnaryExpressionTrait
protected function parseBooleanNot(Expr\BooleanNot $expr): string
{
$pythonOperator = $this->parsePythonUnaryOperator($expr);
if ($pythonOperator !== null) {
return $pythonOperator;
}
$this->assertExprCanBeUsedAsCondition($expr->expr, 'boolean operand');
return '!(' . $this->convertBoolExpr(
$this->parseExprAsValue($expr->expr),
@ -69,6 +77,10 @@ trait UnaryExpressionTrait
protected function parseUnaryMinus(Expr\UnaryMinus $expr): string
{
$pythonOperator = $this->parsePythonUnaryOperator($expr);
if ($pythonOperator !== null) {
return $pythonOperator;
}
$type = $this->detectTypeOfExpr($expr->expr);
$this->assertExprCanBeUsedAsValue($expr->expr, 'unary operand');
if ($type === Type::BIGFLOAT) {
@ -100,6 +112,10 @@ trait UnaryExpressionTrait
protected function parseUnaryPlus(Expr\UnaryPlus $expr): string
{
$pythonOperator = $this->parsePythonUnaryOperator($expr);
if ($pythonOperator !== null) {
return $pythonOperator;
}
$this->assertExprCanBeUsedAsValue($expr->expr, 'unary operand');
return $this->parseExprAsValue($expr->expr);
}

@ -452,6 +452,7 @@ trait UniversalMethodCall
'toBigInt' => 'php::BigInt::newInstance(' . $receiver . ')',
'toBigFloat' => 'php::BigFloat::newInstance(' . $receiver . ')',
'toDecimal' => 'php::Decimal::newInstance(' . $receiver . ')',
'toPlainValue' => 'php::toPlainValue(' . $receiver . ')',
'toAny' => $receiver,
default => $receiver,
};

@ -0,0 +1,652 @@
<?php
namespace TypePhp\Python;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\NodeAbstract;
use TypePhp\Type;
trait PythonModuleTrait
{
/** @var array<string, string> TypePHP constructor sugar to the existing phpy facade class. */
private const PYTHON_CONSTRUCTOR_CLASSES = [
'list' => 'PyList',
'dict' => 'PyDict',
'tuple' => 'PyTuple',
'set' => 'PySet',
'str' => 'PyStr',
'object' => 'PyObject',
];
/** @var array<string, string> Python calls whose precise phpy wrapper is known statically. */
private const PYTHON_BUILTIN_RETURN_CLASSES = [
...self::PYTHON_CONSTRUCTOR_CLASSES,
'int' => 'PyObject',
'float' => 'PyObject',
'bytes' => 'PyObject',
'type' => 'PyType',
];
/** @var array<string, true> Builtins exposed as explicit PyCore methods. */
private const PYTHON_CORE_FUNCTIONS = [
'int' => true,
'float' => true,
'bytes' => true,
'scalar' => true,
];
/** @var array<string, string> Lowercase alias to case-sensitive Python module name. */
protected array $pythonModuleAliases = [];
/** @var array<string, int> Case-sensitive Python module name to generated slot ID. */
protected array $pythonModuleMap = [];
protected int $pythonModuleIndex = 0;
protected bool $pythonRuntimeUsed = false;
/** Return true when the expression is statically known to hold a phpy proxy. */
protected function isPythonObjectExpr(NodeAbstract $expr): bool
{
$class = $this->detectClassOfExpr($expr);
if ($class === '') {
return false;
}
return strcasecmp($class, 'PyObject') === 0
|| $this->isObjectClassStaticallyAssignableTo($class, 'PyObject');
}
/**
* Python members are resolved by the Python VM. Only methods explicitly
* registered on the phpy wrapper may use a cached Zend method pointer;
* every other name must reach PyObject::__call().
*/
protected function isPythonDynamicMethodCall(NodeAbstract $receiver, string $method): bool
{
if (!$this->isPythonObjectExpr($receiver)) {
return false;
}
$class = $this->detectClassOfExpr($receiver);
return $class === '' || !\TypePhp\Resolver\Reflection::hasMethod($class, $method);
}
protected function getPythonBinaryOperator(Expr\BinaryOp $expr): ?string
{
return match (true) {
$expr instanceof Expr\BinaryOp\Plus => 'add',
$expr instanceof Expr\BinaryOp\Minus => 'sub',
$expr instanceof Expr\BinaryOp\Mul => 'mul',
$expr instanceof Expr\BinaryOp\Div => 'truediv',
$expr instanceof Expr\BinaryOp\Mod => 'mod',
$expr instanceof Expr\BinaryOp\Pow => 'pow',
$expr instanceof Expr\BinaryOp\ShiftLeft => 'lshift',
$expr instanceof Expr\BinaryOp\ShiftRight => 'rshift',
$expr instanceof Expr\BinaryOp\BitwiseAnd => 'and_',
$expr instanceof Expr\BinaryOp\BitwiseOr => 'or_',
$expr instanceof Expr\BinaryOp\BitwiseXor => 'xor',
$expr instanceof Expr\BinaryOp\Equal => 'eq',
$expr instanceof Expr\BinaryOp\NotEqual => 'ne',
$expr instanceof Expr\BinaryOp\Smaller => 'lt',
$expr instanceof Expr\BinaryOp\SmallerOrEqual => 'le',
$expr instanceof Expr\BinaryOp\Greater => 'gt',
$expr instanceof Expr\BinaryOp\GreaterOrEqual => 'ge',
$expr instanceof Expr\BinaryOp\Identical => 'is_',
$expr instanceof Expr\BinaryOp\NotIdentical => 'is_not',
default => null,
};
}
protected function isPythonBinaryOperatorExpr(NodeAbstract $expr): bool
{
return $expr instanceof Expr\BinaryOp
&& $this->getPythonBinaryOperator($expr) !== null
&& ($this->isPythonObjectExpr($expr->left) || $this->isPythonObjectExpr($expr->right));
}
protected function pythonOperatorReturnsBool(Expr\BinaryOp $expr): bool
{
return $expr instanceof Expr\BinaryOp\Equal
|| $expr instanceof Expr\BinaryOp\NotEqual
|| $expr instanceof Expr\BinaryOp\Smaller
|| $expr instanceof Expr\BinaryOp\SmallerOrEqual
|| $expr instanceof Expr\BinaryOp\Greater
|| $expr instanceof Expr\BinaryOp\GreaterOrEqual
|| $expr instanceof Expr\BinaryOp\Identical
|| $expr instanceof Expr\BinaryOp\NotIdentical;
}
/**
* Lower a Python operation through the standard operator module.
* Braced ArgList elements are sequenced by C++17; call operands that can
* emit statements are materialized first by parseOrderedOperand().
*/
protected function parsePythonBinaryOperator(Expr\BinaryOp $expr): ?string
{
if (!$this->isPythonBinaryOperatorExpr($expr)) {
return null;
}
$left = $this->parseOrderedOperand($expr->left, false);
$right = $this->parseOrderedOperand($expr->right, false);
$call = $this->getPythonModuleExpression('operator')
. '.call(' . $this->getLiteralString($this->getPythonBinaryOperator($expr))
. ', php::ArgList{' . $left . ', ' . $right . '})';
return $this->pythonOperatorReturnsBool($expr)
? $this->convertPythonResultToBool($call)
: $call;
}
protected function convertPythonResultToBool(string $expression): string
{
$this->markPythonRuntimeUsed();
$scalar = 'php::call(' . $this->getClassEntryPtr('PyCore') . ', '
. $this->getFuncPtr('PyCore::scalar') . ', php::ArgList{' . $expression . '})';
return 'php::toBool(' . $scalar . ')';
}
protected function detectPythonOperatorReturnType(NodeAbstract $expr): ?string
{
if ($this->isPythonBinaryOperatorExpr($expr)) {
return $this->pythonOperatorReturnsBool($expr) ? Type::BOOL : Type::OBJECT;
}
if ($this->isPythonUnaryOperatorExpr($expr)) {
return $expr instanceof Expr\BooleanNot ? Type::BOOL : Type::OBJECT;
}
return null;
}
protected function detectPythonOperatorReturnClass(NodeAbstract $expr): ?string
{
if ($this->isPythonBinaryOperatorExpr($expr)) {
return $this->pythonOperatorReturnsBool($expr) ? null : 'PyObject';
}
if ($this->isPythonUnaryOperatorExpr($expr)) {
return $expr instanceof Expr\BooleanNot ? null : 'PyObject';
}
return null;
}
protected function getPythonUnaryOperator(NodeAbstract $expr): ?string
{
return match (true) {
$expr instanceof Expr\UnaryMinus => 'neg',
$expr instanceof Expr\UnaryPlus => 'pos',
$expr instanceof Expr\BitwiseNot => 'invert',
$expr instanceof Expr\BooleanNot => 'not_',
default => null,
};
}
protected function isPythonUnaryOperatorExpr(NodeAbstract $expr): bool
{
return $this->getPythonUnaryOperator($expr) !== null
&& $this->isPythonObjectExpr($expr->expr);
}
protected function parsePythonUnaryOperator(NodeAbstract $expr): ?string
{
if (!$this->isPythonUnaryOperatorExpr($expr)) {
return null;
}
$operand = $this->parseOrderedOperand($expr->expr, false);
$call = $this->getPythonModuleExpression('operator')
. '.call(' . $this->getLiteralString($this->getPythonUnaryOperator($expr))
. ', php::ArgList{' . $operand . '})';
return $expr instanceof Expr\BooleanNot ? $this->convertPythonResultToBool($call) : $call;
}
protected function convertPythonObjectToBool(NodeAbstract $expr, string $parsed): ?string
{
if (!$this->isPythonObjectExpr($expr)) {
return null;
}
$call = $this->getPythonModuleExpression('operator')
. '.call(' . $this->getLiteralString('truth') . ', php::ArgList{' . $parsed . '})';
return $this->convertPythonResultToBool($call);
}
protected function getPythonAssignOperator(Expr\AssignOp $expr): ?string
{
return match (true) {
$expr instanceof Expr\AssignOp\Plus => 'iadd',
$expr instanceof Expr\AssignOp\Minus => 'isub',
$expr instanceof Expr\AssignOp\Mul => 'imul',
$expr instanceof Expr\AssignOp\Div => 'itruediv',
$expr instanceof Expr\AssignOp\Mod => 'imod',
$expr instanceof Expr\AssignOp\Pow => 'ipow',
$expr instanceof Expr\AssignOp\ShiftLeft => 'ilshift',
$expr instanceof Expr\AssignOp\ShiftRight => 'irshift',
$expr instanceof Expr\AssignOp\BitwiseAnd => 'iand',
$expr instanceof Expr\AssignOp\BitwiseOr => 'ior',
$expr instanceof Expr\AssignOp\BitwiseXor => 'ixor',
default => null,
};
}
/**
* Lower Python compound assignments through operator.i*(). The target
* receiver and key are materialized before the RHS, then the returned
* Python object is written back through the original PHP lvalue protocol.
*/
protected function parsePythonAssignOperator(Expr\AssignOp $expr): ?string
{
$method = $this->getPythonAssignOperator($expr);
if ($method === null || !$this->isPythonObjectExpr($expr->var)) {
return null;
}
$writeBack = null;
if ($this->isVarExpr($expr->var)) {
$left = $this->parseWritableIdentifier($expr->var);
$writeBack = static fn(string $value): string => $left . ' = ' . $value;
} elseif ($expr->var instanceof Expr\ArrayDimFetch) {
if ($expr->var->dim === null) {
$this->fatalError($expr->var, 'Cannot use [] for a Python compound assignment');
}
$container = $this->parseOrderedOperand($expr->var->var, false);
$key = $this->parseOrderedOperand($expr->var->dim, false);
$left = $container . '.item(' . $key . ', false)';
$writeBack = static fn(string $value): string => $container . '.offsetSet(' . $key . ', ' . $value . ')';
} elseif ($expr->var instanceof Expr\PropertyFetch && $this->isIdExpr($expr->var->name)) {
$receiver = $this->parseOrderedOperand($expr->var->var, false);
$property = $this->identifierToStr($expr->var->name, literal: true);
$left = $receiver . '.attr(' . $property . ', php::AttrMode::Get)';
$writeBack = static fn(string $value): string => 'typephp_write_property_scoped('
. $receiver . ', ' . $property . ', ' . $value . ', nullptr)';
} else {
return null;
}
$right = $this->parseOrderedOperand($expr->expr, false);
$call = $this->getPythonModuleExpression('operator')
. '.call(' . $this->getLiteralString($method)
. ', php::ArgList{' . $left . ', ' . $right . '})';
if ($this->isVarExpr($expr->var)) {
return $writeBack($call);
}
$result = $this->addTmpVar(Type::OBJECT);
return '((' . $result . ' = ' . $call . ', ' . $writeBack($result) . '), ' . $result . ')';
}
protected function resetPythonModuleAliases(): void
{
$this->pythonModuleAliases = [];
}
protected function parsePythonUse(Node\UseItem $use, string $name, NodeAbstract $statement): bool
{
$parts = explode('\\', trim($name, '\\'));
if (strcasecmp($parts[0] ?? '', 'python') !== 0) {
return false;
}
if (count($parts) < 2) {
$this->fatalError($statement, 'The special `python` namespace must be followed by a module name');
}
// PHP namespace separators express Python's dotted module path only
// in source syntax; PyCore::import() expects the canonical Python name.
$module = implode('.', array_slice($parts, 1));
$alias = $use->alias?->toString() ?? $parts[array_key_last($parts)];
$aliasKey = strtolower($alias);
if (isset($this->pythonModuleAliases[$aliasKey]) && $this->pythonModuleAliases[$aliasKey] !== $module) {
$this->fatalError($use, "Python module alias `{$alias}` is already used");
}
if (isset($this->useAliases[$alias]) || isset($this->useFunctions[$alias]) || isset($this->useConstants[$alias])) {
$this->fatalError($use, "Python module alias `{$alias}` conflicts with an existing use symbol");
}
$this->pythonModuleAliases[$aliasKey] = $module;
return true;
}
protected function hasPythonModuleAlias(string $alias): bool
{
return isset($this->pythonModuleAliases[strtolower($alias)]);
}
protected function resolvePythonModuleAlias(NodeAbstract $class): ?string
{
if (!$this->isNameExpr($class)) {
return null;
}
$alias = $this->parseIdentifier($class);
if (str_contains($alias, '\\')) {
return null;
}
return $this->pythonModuleAliases[strtolower($alias)] ?? null;
}
protected function getPythonModuleId(string $module): int
{
if (isset($this->pythonModuleMap[$module])) {
return $this->pythonModuleMap[$module];
}
$id = $this->pythonModuleIndex++;
$this->pythonModuleMap[$module] = $id;
$this->markPythonRuntimeUsed();
$this->getFuncId('PyCore::import');
$this->getLiteralString('PyCore::import');
$this->getLiteralString($module);
return $id;
}
protected function markPythonRuntimeUsed(): void
{
if ($this->pythonRuntimeUsed) {
return;
}
$this->pythonRuntimeUsed = true;
// Register runtime Zend symbols during conversion so generated map
// sizes are final before headers are emitted.
$this->getClassId('PyCore');
$this->getFuncId('PyCore::setOptions');
$this->getLiteralString('PyCore');
$this->getLiteralString('PyCore::setOptions');
$this->getLiteralString('return_as_object');
}
protected function withPythonRuntimeConfigured(string $expression): string
{
return '(' . self::PREFIX . 'configure_python_runtime(), ' . $expression . ')';
}
protected function getPythonModuleExpression(string $module): string
{
return 'php_get_python_module(' . $this->getPythonModuleId($module) . ', '
. $this->getLiteralString($module) . ')';
}
protected function resolvePythonBuiltinName(NodeAbstract $name): ?string
{
if (!$this->isNameExpr($name) && !$this->isFullNameExpr($name)) {
return null;
}
$parts = explode('\\', trim($this->parseIdentifier($name), '\\'));
if (strcasecmp($parts[0] ?? '', 'python') !== 0) {
return null;
}
if (count($parts) !== 2 || $parts[1] === '') {
$this->fatalError($name, 'Python builtins must use the form `python\\name()`');
}
return $parts[1];
}
protected function parsePythonBuiltinCall(Expr\FuncCall $expr): ?string
{
$builtin = $this->resolvePythonBuiltinName($expr->name);
if ($builtin === null) {
return null;
}
if ($expr->isFirstClassCallable()) {
$this->fatalError($expr, 'Python builtins do not support first-class callable syntax yet');
}
$this->markPythonRuntimeUsed();
// This is a deliberately closed map. In particular, PyDict's PHP-array
// constructor is not equivalent to Python's dict(iterable) builtin.
$constructorClass = self::PYTHON_CONSTRUCTOR_CLASSES[$builtin] ?? null;
if ($constructorClass !== null) {
$classEntry = $this->getClassEntryPtr($constructorClass);
if ($expr->args === []) {
return $this->withPythonRuntimeConfigured('php::newObject(' . $classEntry . ')');
}
return $this->withPythonRuntimeConfigured(
'php::newObject(' . $classEntry . ', '
. $this->parseCallArgs($expr->args, '__construct', $constructorClass) . ')'
);
}
// Explicit PyCore methods either preserve a Python wrapper by design,
// or (`scalar`) explicitly leave Python's object-preserving rules.
if (isset(self::PYTHON_CORE_FUNCTIONS[$builtin])) {
$callable = $this->getClassEntryPtr('PyCore') . ', '
. $this->getFuncPtr('PyCore::' . $builtin);
if ($expr->args === []) {
return $this->withPythonRuntimeConfigured('php::call(' . $callable . ')');
}
return $this->withPythonRuntimeConfigured(
$this->genRuntimeFunctionCall($callable, $expr->args, $builtin, 'PyCore')
);
}
$target = $this->getPythonModuleExpression('builtins');
$name = $this->getLiteralString($builtin);
if ($expr->args === []) {
return $target . '.call(' . $name . ')';
}
return $target . '.call(' . $name . ', ' . $this->parseCallArgs($expr->args) . ')';
}
protected function detectPythonExpressionReturnType(NodeAbstract $expr): ?string
{
if ($expr instanceof Expr\StaticCall && $this->resolvePythonModuleAlias($expr->class) !== null) {
return Type::OBJECT;
}
if ($expr instanceof Expr\StaticPropertyFetch && $this->resolvePythonModuleAlias($expr->class) !== null) {
return Type::OBJECT;
}
if ($expr instanceof Expr\MethodCall && $this->isPythonObjectExpr($expr->var)) {
if (!$this->isIdExpr($expr->name)
|| $this->isPythonDynamicMethodCall($expr->var, $this->parseIdentifier($expr->name))
) {
return Type::OBJECT;
}
return null;
}
if ($expr instanceof Expr\PropertyFetch && $this->isPythonObjectExpr($expr->var)) {
return Type::OBJECT;
}
if ($expr instanceof Expr\ArrayDimFetch && $this->isPythonObjectExpr($expr->var)) {
return Type::OBJECT;
}
if ($expr instanceof Expr\FuncCall
&& $expr->name instanceof NodeAbstract
&& !$this->isNameExpr($expr->name)
&& $this->isPythonObjectExpr($expr->name)
) {
return Type::OBJECT;
}
if (!$expr instanceof Expr\FuncCall) {
return null;
}
$builtin = $this->resolvePythonBuiltinName($expr->name);
if ($builtin === null) {
return null;
}
if ($builtin === 'scalar') {
return Type::VAR;
}
return Type::OBJECT;
}
protected function detectPythonExpressionReturnClass(NodeAbstract $expr): ?string
{
if ($expr instanceof Expr\StaticCall && $this->resolvePythonModuleAlias($expr->class) !== null) {
return 'PyObject';
}
if ($expr instanceof Expr\StaticPropertyFetch && $this->resolvePythonModuleAlias($expr->class) !== null) {
return 'PyObject';
}
if ($expr instanceof Expr\MethodCall && $this->isPythonObjectExpr($expr->var)) {
if (!$this->isIdExpr($expr->name)
|| $this->isPythonDynamicMethodCall($expr->var, $this->parseIdentifier($expr->name))
) {
return 'PyObject';
}
return null;
}
if ($expr instanceof Expr\PropertyFetch && $this->isPythonObjectExpr($expr->var)) {
return 'PyObject';
}
if ($expr instanceof Expr\ArrayDimFetch && $this->isPythonObjectExpr($expr->var)) {
return 'PyObject';
}
if ($expr instanceof Expr\FuncCall
&& $expr->name instanceof NodeAbstract
&& !$this->isNameExpr($expr->name)
&& $this->isPythonObjectExpr($expr->name)
) {
return 'PyObject';
}
if (!$expr instanceof Expr\FuncCall) {
return null;
}
$builtin = $this->resolvePythonBuiltinName($expr->name);
if ($builtin === null) {
return null;
}
if ($builtin === 'scalar') {
return null;
}
return self::PYTHON_BUILTIN_RETURN_CLASSES[$builtin] ?? 'PyObject';
}
protected function parsePythonModuleStaticCall(Expr\StaticCall $expr): ?string
{
if (!$this->isIdExpr($expr->name)) {
return null;
}
$module = $this->resolvePythonModuleAlias($expr->class);
if ($module === null) {
return null;
}
$method = $this->parseIdentifier($expr->name);
$target = $this->getPythonModuleExpression($module);
$methodName = $this->getLiteralString($method);
if ($expr->args === []) {
return $target . '.call(' . $methodName . ')';
}
return $target . '.call(' . $methodName . ', ' . $this->parseCallArgs($expr->args) . ')';
}
protected function parsePythonModuleStaticPropertyFetch(Expr\StaticPropertyFetch $expr): ?string
{
if (!$this->isIdExpr($expr->name)) {
return null;
}
$module = $this->resolvePythonModuleAlias($expr->class);
if ($module === null) {
return null;
}
return $this->getPythonModuleExpression($module)
. '.attr(' . $this->getLiteralString($this->parseIdentifier($expr->name)) . ')';
}
protected function rejectPythonModuleClassConstantFetch(Expr\ClassConstFetch $expr): void
{
if (!$this->isIdExpr($expr->name) || $this->resolvePythonModuleAlias($expr->class) === null) {
return;
}
$alias = $this->parseIdentifier($expr->class);
$member = $this->parseIdentifier($expr->name);
$this->fatalError(
$expr,
"Python module value `{$alias}::{$member}` must use `{$alias}::\${$member}`",
);
}
protected function genPythonModuleDataDeclarations(): string
{
if (!$this->pythonRuntimeUsed) {
return '';
}
$code = 'extern THREAD_LOCAL bool ' . self::PREFIX . 'python_runtime_configured;' . PHP_EOL
. 'void ' . self::PREFIX . 'configure_python_runtime();' . PHP_EOL;
if ($this->pythonModuleMap !== []) {
$code .= 'extern THREAD_LOCAL zval ' . self::PREFIX . 'python_module_map['
. count($this->pythonModuleMap) . '];' . PHP_EOL
. 'php::Object ' . self::PREFIX
. 'get_python_module(int module_id, const php::Str &module_name);' . PHP_EOL;
}
return $code;
}
protected function genPythonModuleStorage(): string
{
if (!$this->pythonRuntimeUsed) {
return '';
}
$code = "// python runtime \n"
. 'THREAD_LOCAL bool ' . self::PREFIX . 'python_runtime_configured = false;' . PHP_EOL;
if ($this->pythonModuleMap !== []) {
$code .= 'THREAD_LOCAL zval ' . self::PREFIX . 'python_module_map['
. count($this->pythonModuleMap) . ']{};' . PHP_EOL;
}
return $code;
}
protected function genPythonModuleGetter(): string
{
if (!$this->pythonRuntimeUsed) {
return '';
}
$pyCoreClass = $this->getClassEntryPtr('PyCore');
$setOptionsFunction = $this->getFuncPtr('PyCore::setOptions');
$returnAsObject = $this->getLiteralString('return_as_object');
$code = 'void ' . self::PREFIX . 'configure_python_runtime() {' . PHP_EOL
. 'if (EXPECTED(' . self::PREFIX . 'python_runtime_configured)) {' . PHP_EOL
. 'return;' . PHP_EOL
. '}' . PHP_EOL
. 'php::Array options;' . PHP_EOL
. 'options.set(' . $returnAsObject . ', true);' . PHP_EOL
. 'php::call(' . $pyCoreClass . ', ' . $setOptionsFunction . ', php::ArgList{options});' . PHP_EOL
. self::PREFIX . 'python_runtime_configured = true;' . PHP_EOL
. '}' . PHP_EOL . PHP_EOL;
if ($this->pythonModuleMap === []) {
return $code;
}
$importFunction = $this->getFuncPtr('PyCore::import');
return $code
. 'php::Object ' . self::PREFIX
. 'get_python_module(int module_id, const php::Str &module_name) {' . PHP_EOL
. self::PREFIX . 'configure_python_runtime();' . PHP_EOL
. 'zval *module = &' . self::PREFIX . 'python_module_map[module_id];' . PHP_EOL
. 'if (UNEXPECTED(Z_ISUNDEF_P(module))) {' . PHP_EOL
. 'auto ce = ' . $pyCoreClass . ';' . PHP_EOL
. 'auto fn = ' . $importFunction . ';' . PHP_EOL
. 'php::Variant imported = php::call(ce, fn, php::ArgList{module_name});' . PHP_EOL
. '(void) php::Object(imported);' . PHP_EOL
. 'imported.moveTo(module);' . PHP_EOL
. '}' . PHP_EOL
. 'return php::Object(module);' . PHP_EOL
. '}' . PHP_EOL . PHP_EOL;
}
protected function genPythonModuleCleanup(): string
{
if (!$this->pythonRuntimeUsed) {
return '';
}
$code = '';
if ($this->pythonModuleMap !== []) {
$code .= 'for (zval &module : ' . self::PREFIX . 'python_module_map) {' . PHP_EOL
. 'if (!Z_ISUNDEF(module)) {' . PHP_EOL
. 'zval_ptr_dtor(&module);' . PHP_EOL
. 'ZVAL_UNDEF(&module);' . PHP_EOL
. '}' . PHP_EOL
. '}' . PHP_EOL;
}
return $code . self::PREFIX . 'python_runtime_configured = false;' . PHP_EOL;
}
}

@ -72,6 +72,9 @@ trait DeclarationSymbolTrait
foreach ($v2->uses as $use) {
$id = $this->parseIdentifier($use->name);
$type = $use->type !== Node\Stmt\Use_::TYPE_UNKNOWN ? $use->type : $v2->type;
if ($type === Node\Stmt\Use_::TYPE_NORMAL && $this->parsePythonUse($use, $id, $v2)) {
continue;
}
if ($type === Node\Stmt\Use_::TYPE_FUNCTION) {
$lastIndex = strrpos($id, '\\');
$fn = substr($id, $lastIndex + 1);
@ -99,6 +102,10 @@ trait DeclarationSymbolTrait
} elseif ($idLower === 'bigint_types') {
$this->bigintTypes = true;
} else {
$alias = $use->alias?->toString() ?? substr($id, (int) strrpos('\\' . $id, '\\'));
if ($this->hasPythonModuleAlias($alias)) {
$this->fatalError($use, "Use alias `{$alias}` conflicts with a Python module alias");
}
$this->useNamespaces[] = $id;
if ($use->alias) {
$this->useAliases[$use->alias->toString()] = $id;
@ -122,4 +129,3 @@ trait DeclarationSymbolTrait
}
}

@ -746,6 +746,11 @@ class Translator extends Preprocessor
$funcCount = max(1, count($this->funcMap));
$lines[] = 'extern THREAD_LOCAL zend_function *' . self::PREFIX . self::FUNC_MAP . '[' . $funcCount . '];' . PHP_EOL;
$pythonModuleDeclarations = $this->genPythonModuleDataDeclarations();
if ($pythonModuleDeclarations !== '') {
$lines[] = $pythonModuleDeclarations;
}
$propCount = max(1, count($this->propMap));
$lines[] = 'extern THREAD_LOCAL uint32_t ' . self::PREFIX . self::PROP_MAP . '[' . $propCount . '];' . PHP_EOL;
@ -809,6 +814,8 @@ class Translator extends Preprocessor
$code .= "// func \n";
$code .= 'THREAD_LOCAL zend_function *' . self::PREFIX . self::FUNC_MAP . '[' . max(1, count($this->funcMap)) . '];' . PHP_EOL;
$code .= $this->genPythonModuleStorage();
$code .= "// property \n";
$code .= 'THREAD_LOCAL uint32_t ' . self::PREFIX . self::PROP_MAP . '[' . max(1, count($this->propMap)) . '];' . PHP_EOL;
@ -846,6 +853,8 @@ uint32_t php_get_prop(int prop_id, const php::Str &prop_name, int class_id, cons
CODE;
$code .= "\n\n";
$code .= $this->genPythonModuleGetter();
$code .= "// literal strings \n";
if ($this->literalStrings) {
$code .= Type::STR . ' ' . self::LITERAL_STRINGS . '[] = {' . PHP_EOL;
@ -1007,6 +1016,8 @@ CODE;
$code .= $name . '.unset();' . PHP_EOL;
}
$code .= $this->genPythonModuleCleanup();
$code .= '// class array constants' . PHP_EOL;
foreach ($this->getClassLikesWithConstants() as $classDef) {
foreach ($classDef->constants as $constant) {

@ -48,11 +48,19 @@ function main()
echo $error::class, "\n";
}
try {
$value = makeUnsetReassignOtherDynamic();
echo "invalid assignment after null accepted\n";
} catch (Throwable $error) {
echo $error::class, "\n";
}
$value = makeUnsetReassignExpected();
var_dump($value->value());
}
?>
--EXPECT--
TypeError
null assignment accepted
TypeError
string(8) "expected"

@ -1,5 +1,5 @@
--TEST--
unset typed object reads as null and accepts a valid reassignment
typed object accepts null as its empty state and retains its declared class constraint
--FILE--
<?php
class UnsetTypedObjectValue
@ -25,10 +25,10 @@ function makeUnsetTypedObjectValue(): UnsetTypedObjectValue
function main()
{
$value = makeUnsetTypedObjectValue();
unset($value);
var_dump(($value = null));
var_dump(@$value === null);
var_dump(@$value instanceof UnsetTypedObjectValue);
var_dump($value === null);
var_dump($value instanceof UnsetTypedObjectValue);
var_dump(isset($value));
try {
@ -39,11 +39,21 @@ function main()
$value = makeUnsetTypedObjectValue();
var_dump($value->value());
unset($value);
$value = null;
var_dump($value);
$value = makeUnsetTypedObjectValue();
var_dump($value->readProperty());
}
?>
--EXPECT--
NULL
bool(true)
bool(false)
bool(false)
Error
string(5) "value"
NULL
int(1)

@ -0,0 +1,40 @@
--TEST--
TypePHP values convert recursively at the Python call boundary
--SKIPIF--
<?php
if (!extension_loaded('phpy')) {
die('skip phpy extension is not loaded');
}
?>
--FILE--
<?php
function main(): void
{
$values = [
null,
true,
42,
1.5,
'你好',
[],
[1, 2],
['name' => 'TypePHP'],
['nested' => [1, ['ok' => true]]],
];
foreach ($values as $value) {
echo python\scalar(python\repr($value))->toString(), "\n";
}
}
?>
--EXPECT--
None
True
42
1.5
'你好'
[]
[1, 2]
{'name': 'TypePHP'}
{'nested': [1, {'ok': True}]}

@ -0,0 +1,41 @@
--TEST--
Python arguments evaluate left-to-right once and accept TypePHP callables
--SKIPIF--
<?php
if (!extension_loaded('phpy')) {
die('skip phpy extension is not loaded');
}
?>
--FILE--
<?php
use Python\sys;
use Python\typephp_protocol as protocol;
function mark(int $value): int
{
echo "mark:$value\n";
return $value;
}
function main(): void
{
$power = python\pow(mark(2), mark(3));
var_dump(python\scalar($power)->toInt());
$mapped = python\map(fn (int $value): int => $value * 2, [1, 2, 3]);
var_dump(python\scalar(python\sum($mapped))->toInt());
sys::$path->append(__DIR__ . '/lib');
$callbackResult = protocol::callback_with_kwargs(
fn (string $left, int $right): string => "$left:$right"
);
var_dump(python\scalar($callbackResult)->toString());
}
?>
--EXPECT--
mark:2
mark:3
int(8)
int(12)
string(6) "left:7"

@ -0,0 +1,29 @@
--TEST--
Python builtin and constructor errors preserve their runtime exception types
--SKIPIF--
<?php
if (!extension_loaded('phpy')) {
die('skip phpy extension is not loaded');
}
?>
--FILE--
<?php
function main(): void
{
try {
python\len();
} catch (PyError $error) {
echo "python argument error\n";
}
try {
python\list('not an array');
} catch (Error $error) {
echo "phpy constructor error\n";
}
}
?>
--EXPECT--
python argument error
phpy constructor error

@ -0,0 +1,54 @@
--TEST--
Python builtins and constructors use phpy objects
--SKIPIF--
<?php
if (!extension_loaded('phpy')) {
die('skip phpy extension is not loaded');
}
?>
--FILE--
<?php
function main(): void
{
$list = python\list([1, 2, 3]);
$dict = Python\dict(['answer' => 42]);
$tuple = PYTHON\tuple([1, 2]);
$set = python\set([1, 2]);
$str = python\str(123);
$int = python\int('42');
$object = python\object('value');
$bytes = python\bytes('value');
var_dump(get_class($list));
var_dump(get_class($dict));
var_dump(get_class($tuple));
var_dump(get_class($set));
var_dump(get_class($str));
var_dump(get_class($int));
var_dump(get_class($object));
var_dump(get_class($bytes));
var_dump(get_class(python\len($list)));
var_dump(get_class(python\bool(0)));
var_dump(python\scalar($int)->toInt());
// The constructor syntax is deliberately phpy's PHP-array conversion,
// not CPython's dict(iterable-of-pairs) constructor.
var_dump(python\scalar($dict['answer'])->toInt());
python\print('hello from python');
}
?>
--EXPECT--
string(6) "PyList"
string(6) "PyDict"
string(7) "PyTuple"
string(5) "PySet"
string(5) "PyStr"
string(8) "PyObject"
string(8) "PyObject"
string(8) "PyObject"
string(8) "PyObject"
string(8) "PyObject"
int(42)
int(42)
hello from python

@ -0,0 +1,22 @@
--TEST--
Python builtin names remain case-sensitive
--SKIPIF--
<?php
if (!extension_loaded('phpy')) {
die('skip phpy extension is not loaded');
}
?>
--FILE--
<?php
function main(): void
{
try {
python\Len([1, 2, 3]);
} catch (PyError $error) {
echo "unknown builtin\n";
}
}
?>
--EXPECT--
unknown builtin

@ -0,0 +1,22 @@
--TEST--
Python constructor-only programs preserve proxy results as PyObject
--SKIPIF--
<?php
if (!extension_loaded('phpy')) {
die('skip phpy extension is not loaded');
}
?>
--FILE--
<?php
function main(): void
{
$list = python\list([42]);
$value = $list[0];
var_dump(get_class($value));
var_dump(python\scalar($value)->toInt());
}
?>
--EXPECT--
string(8) "PyObject"
int(42)

@ -0,0 +1,31 @@
--TEST--
Python conversion rejects invalid UTF-8 and recursive PHP arrays safely
--SKIPIF--
<?php
if (!extension_loaded('phpy')) {
die('skip phpy extension is not loaded');
}
?>
--FILE--
<?php
function main(): void
{
try {
python\repr("\xff");
} catch (PyError $error) {
echo "invalid utf8\n";
}
$recursive = [];
$recursive['self'] = &$recursive;
try {
python\repr($recursive);
} catch (PyError $error) {
echo "recursive php array\n";
}
}
?>
--EXPECT--
invalid utf8
recursive php array

@ -0,0 +1 @@
; Intentionally empty: used to verify the optional phpy runtime boundary.

@ -0,0 +1,17 @@
class ProtocolObject:
def __init__(self):
self.name = "initial"
self.values = [10, 20, 30]
def greet(self, prefix, suffix="!"):
return f"{prefix} {self.name}{suffix}"
def __call__(self, left, right=0):
return left + right
def protocol_object():
return ProtocolObject()
def callback_with_kwargs(callback):
return callback("left", right=7)

@ -0,0 +1,19 @@
--TEST--
Using a Python module without phpy raises a catchable PHP Error
--ENV--
PHPRC=tests/compiler/python/empty.ini
--FILE--
<?php
use python\math;
function main(): void
{
try {
var_dump(math::$pi);
} catch (Error $error) {
echo get_class($error), "\n";
}
}
?>
--EXPECT--
Error

@ -0,0 +1,19 @@
--TEST--
Python module aliases lazily import through phpy
--SKIPIF--
<?php
if (!extension_loaded('phpy')) {
die('skip phpy extension is not loaded');
}
?>
--FILE--
<?php
use Python\math;
function main(): void
{
var_dump(python\scalar(math::$pi)->toFloat() > 3.14);
}
?>
--EXPECT--
bool(true)

@ -0,0 +1,23 @@
--TEST--
Python module scalar results remain wrapped objects
--SKIPIF--
<?php
if (!extension_loaded('phpy')) {
die('skip phpy extension is not loaded');
}
?>
--FILE--
<?php
use python\math;
function main(): void
{
$result = math::sqrt(4);
var_dump(get_class($result));
var_dump(python\scalar($result)->toFloat());
}
?>
--EXPECT--
string(8) "PyObject"
float(2)

@ -0,0 +1,99 @@
--TEST--
Python proxies support attributes, methods, items, iteration and callable objects
--SKIPIF--
<?php
if (!extension_loaded('phpy')) {
die('skip phpy extension is not loaded');
}
?>
--FILE--
<?php
use Python\sys;
use Python\typephp_protocol as protocol;
function scalarValue(PyObject $value): mixed
{
return python\scalar($value);
}
function mark(string $name, int $value): int
{
echo $name;
return $value;
}
function main(): void
{
sys::$path->append(__DIR__ . '/lib');
$object = protocol::protocol_object();
var_dump(scalarValue($object->name));
$object->name = 'changed';
var_dump(scalarValue($object->greet('hello', suffix: '?')));
var_dump(scalarValue($object(mark('L', 3), right: mark('R', 4))));
$args = [5];
var_dump(scalarValue($object(...$args, right: 6)));
$values = $object->values;
var_dump(isset($values[-1]));
var_dump(isset($values[-4]));
var_dump(scalarValue($values[-1]));
$values[-1] = 40;
unset($values[-2]);
$values[0] += python\int(5);
$object->counter = python\int(2);
$object->counter += 3;
var_dump(scalarValue($object->counter));
foreach ($values as $key => $value) {
echo $key, ':', scalarValue($value), "\n";
}
$dict = python\dict(['first' => 1, 'second' => 2]);
var_dump(isset($dict['missing']));
$dict['third'] = 3;
unset($dict['first']);
foreach ($dict as $key => $value) {
echo $key, '=', scalarValue($value), "\n";
}
unset($object->name);
try {
$object->name;
} catch (PyError $error) {
echo "attribute deleted\n";
}
$object->name = 'not callable';
try {
$object->name();
} catch (PyError $error) {
echo "attribute is not callable\n";
}
try {
python\int(42)();
} catch (PyError $error) {
echo "object is not callable\n";
}
}
?>
--EXPECT--
string(7) "initial"
string(14) "hello changed?"
LRint(7)
int(11)
bool(true)
bool(false)
int(30)
int(5)
0:15
1:40
bool(false)
second=2
third=3
attribute deleted
attribute is not callable
object is not callable

@ -0,0 +1,149 @@
--TEST--
Python objects use Python arithmetic, comparison, identity and truthiness protocols
--SKIPIF--
<?php
if (!extension_loaded('phpy')) {
die('skip phpy extension is not loaded');
}
?>
--FILE--
<?php
use Python\math;
function asInt($value): int
{
return python\scalar($value)->toInt();
}
function asFloat($value): float
{
return python\scalar($value)->toFloat();
}
function mark(string $name, int $value): PyObject
{
echo $name;
return python\int($value);
}
function main(): void
{
$seven = python\int(7);
$three = python\int(3);
echo asInt($seven + $three), "\n";
echo asInt($seven - $three), "\n";
echo asInt($seven * $three), "\n";
echo asFloat($seven / 2), "\n";
echo asInt($seven % $three), "\n";
echo asInt($three ** 3), "\n";
echo asInt($three << 2), "\n";
echo asInt(16 >> $three), "\n";
echo asInt($seven & $three), "\n";
echo asInt($seven | $three), "\n";
echo asInt($seven ^ $three), "\n";
echo asInt(math::sqrt(9) + 1), "\n";
// The Python reflected protocol handles the native left operand.
echo python\scalar(python\repr(1 + python\complex(2, 3)))->toString(), "\n";
var_dump($seven == python\int(7));
var_dump($seven != $three);
var_dump($seven > $three);
var_dump($seven >= python\int(7));
var_dump($three < $seven);
var_dump($three <= python\int(3));
$list = python\list([7]);
$alias = $list;
var_dump($list === $alias);
var_dump($list !== python\list([7]));
$three += 4;
echo asInt($three), "\n";
$compound = python\int(10);
$compound -= 3;
echo asInt($compound), "\n";
$compound *= 2;
echo asInt($compound), "\n";
$compound /= 4;
echo asFloat($compound), "\n";
$compound = python\int(10);
$compound %= 4;
echo asInt($compound), "\n";
$compound **= 3;
echo asInt($compound), "\n";
$compound <<= 2;
echo asInt($compound), "\n";
$compound >>= 1;
echo asInt($compound), "\n";
$compound &= 6;
echo asInt($compound), "\n";
$compound |= 1;
echo asInt($compound), "\n";
$compound ^= 3;
echo asInt($compound), "\n";
echo asInt(-$three), "\n";
echo asInt(+$three), "\n";
echo asInt(~$three), "\n";
if (python\int(0)) {
echo "bad\n";
} else {
echo "false\n";
}
if (!python\list()) {
echo "empty\n";
}
var_dump(python\int(0) || python\int(1));
var_dump(python\int(1) && python\list([1]));
var_dump(python\int(0) xor python\list([1]));
echo asInt(mark('L', 4) + mark('R', 5)), "\n";
}
?>
--EXPECT--
10
4
21
3.5
1
27
12
2
3
7
4
4
(3+3j)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
7
7
14
3.5
2
8
32
16
0
1
2
-7
7
-8
false
empty
bool(true)
bool(true)
bool(true)
LR9

@ -0,0 +1,53 @@
--TEST--
PyObject toPlainValue() explicitly returns a PHP value
--SKIPIF--
<?php
if (!extension_loaded('phpy')) {
die('skip phpy extension is not loaded');
}
?>
--FILE--
<?php
function main(): void
{
$list = python\list([1, 2, 3]);
$integer = python\int(42);
var_dump(toPlainValue($list));
var_dump(toPlainValue($list)->toArray());
var_dump($integer->toPlainValue());
var_dump($integer->toPlainValue()->toInt());
try {
toPlainValue(new stdClass());
} catch (Error $error) {
var_dump(str_contains($error->getMessage(), 'supports PyObject only'));
}
}
function toPlainValue(mixed $value): mixed
{
return $value->toPlainValue();
}
?>
--EXPECT--
array(3) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(3)
}
array(3) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(3)
}
int(42)
int(42)
bool(true)

@ -0,0 +1,13 @@
--TEST--
An unused Python module alias does not import the module
--FILE--
<?php
use python\module_that_does_not_exist;
function main(): void
{
echo "no import\n";
}
?>
--EXPECT--
no import

@ -0,0 +1,36 @@
--TEST--
Trait constants can merge another trait constant with array unpacking
--FILE--
<?php
trait TraitConstantArraySpread
{
private const BASE = [
'list' => 'PyList',
'dict' => 'PyDict',
];
public const MERGED = [
...self::BASE,
'int' => 'PyObject',
];
}
class TraitConstantArraySpreadUser
{
use TraitConstantArraySpread;
}
function main(): void
{
var_dump(TraitConstantArraySpreadUser::MERGED);
}
?>
--EXPECT--
array(3) {
["list"]=>
string(6) "PyList"
["dict"]=>
string(6) "PyDict"
["int"]=>
string(8) "PyObject"
}
Loading…
Cancel
Save