parent
2be13eefcd
commit
069d3e61bb
51 changed files with 2988 additions and 75 deletions
@ -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"; |
||||
} |
||||
@ -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 必须保留独立回归测试。 |
||||
- 不以修改第三方测试期望来掩盖实现差异。 |
||||
@ -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; |
||||
} |
||||
} |
||||
@ -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…
Reference in new issue