51 KiB
TypePHP 与 Python 语言级互调用设计
状态:核心设计已确认,按
python/implementation-plan.md分阶段实施。本文是语法、类型语义、运行时边界和兼容性目标的设计规范;尚未确认的细节继续在文末维护。
1. 目标
TypePHP 应在语言层面提供从 TypePHP 调用 Python 包的能力:
- TypePHP 导入 Python 模块,访问模块成员,调用 Python 函数和类。
- TypePHP 操作 Python 对象,包括属性、方法、下标、迭代、运算符和调用协议。
- TypePHP 函数、闭包和对象可以作为 Python 调用的参数,并允许 Python 在该次动态调用关系中同步回调。
- 两个 VM 在同一进程内直接互调用,不通过 JSON、RPC 或子进程。
- 默认保留 Python 对象身份和类型信息,避免不必要的深拷贝。
- 语法面向普通 TypePHP/PHP 开发者,常规调用不要求理解 CPython C API、GIL 或引用计数。
- 本功能是可选的扩展级能力;不使用 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、Mojo Python types。
2.2 pybind11
pybind11 明确区分对象所有权、返回值策略、解释器生命周期、GIL guard、位置参数和关键字参数。其经验说明:跨语言调用最危险的部分不是调用语法,而是对象生命周期和异常路径。
TypePHP 不应把 pybind11 的所有权策略暴露给普通用户,但运行时必须建立同等严格的内部契约。
参考:pybind11 embedding、pybind11 functions。
2.3 PyO3
PyO3 使用 GIL token 和带生命周期的 Python 对象指针,从类型系统上区分持有对象、借用对象和 GIL 绑定对象。
TypePHP 无需向用户暴露生命周期参数,但 phpy 的 C++ 层应借鉴这一点:所有 CPython API 调用必须能证明当前持有 GIL,所有 PyObject* 必须明确是 owned、borrowed 还是 stolen reference。
参考:PyO3 object model、PyO3 Python object 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 运算符通过标准库operatormodule 调用完整的 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 专用实现必须集中到独立子目录,暂定为:
src/Python/
该目录负责:
python特殊根命名空间识别。- import/module symbol 表。
- Python Zend class/method 名称和逻辑返回类型映射。
- Python 语法糖和静态返回类型映射。
- Python 调用、属性、下标、迭代和运算符的 C++ lowering。
- Python 专用诊断。
通用 Parser、TypeSystem、Optimizer 和 Generator 只允许保留最小、稳定的扩展入口,不应散落 if ($isPython...) 特判。Python 功能未启用时,不加载 Python 专用分析器,也不改变现有代码生成路径。
测试同样独立组织,建议使用:
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 是编译器识别的保留根命名空间:
use python\sys;
use Python\numpy as np;
use python\numpy\linalg as linalg;
分别等价于:
import sys
import numpy as np
import numpy.linalg as linalg
在现有 phpy PHP API 中,语义上对应:
$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()。下列名称只是设计示意:
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:
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-owningzend_function*,清理时可以直接memset。pythonModuleMap保存 phpy 返回的 ZendPyModuleobject zval,不能直接memset覆盖有效对象。- request clean 必须逐项执行
zval_ptr_dtor()并恢复为UNDEF,让 phpy 自己的 Zend object destructor 处理 Python reference 和 GIL。 - import 失败时 slot 保持
UNDEF,不能缓存异常值或半初始化对象。
清理由 TypePHP 使用普通 Zend zval API 完成,不调用 phpy C++ 符号:
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 中的绑定:
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\...是使用pythonModuleMaplazy binding 的语言级 namespace 标记。
示例:
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:
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(...):
$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。建议初版只支持读取;需要写入时使用:
python\setattr(os, 'name', $value); // 伪代码,具体模块值语法仍需确定
8. Python 内置函数与 phpy 语法糖
python\name() 表示调用 Python builtins:
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) |
例如:
$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 构造规则不同。
所有语法糖的映射必须形成封闭、经过测试的表,不能仅凭函数名猜测。
该映射同时决定编译期静态类型:
$list1 = new PyList();
$list2 = python\list();
$dict1 = new PyDict();
$dict2 = python\dict();
$list1与$list2都是PyListtyped object。$dict1与$dict2都是PyDicttyped object。- 两种写法必须使用相同的类型检查、方法解析和 Native Call 优化。
- 语法糖不能退化成
mixed、var或只有基础类型PyObject。 - Python builtin 调用同样遵守对象保持规则,例如
python\len()返回包装 Python int 的PyObject;需要先以toPlainValue()(或兼容入口python\scalar())离开 Python 对象规则,再使用普通 TypePHP 转换得到确定类型。python\print()的 PythonNone结果也保持为PyObject,作为独立语句使用时可直接丢弃。 toPlainValue()和python\scalar()都不是普通 Python builtin 调用,而是明确要求退出 Python 类型规则的转换边界,因此返回 TypePHPvar。- 动态 Python module 成员调用统一返回
PyObject。
9. Python 对象类型
所有无法在编译期确定静态类型的 Python 值统一表示为:
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 属性和方法
$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 转换方法确定类型:
$pyValue = np::int64(42); // PyObject
$value = $pyValue->toPlainValue()->toInt(); // TypePHP int
这里的 toInt() 作用于 toPlainValue() 已返回的 TypePHP 值,并非作用于 PyObject。
10.2 下标
$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 调用对象
$result = $callable($arg1, $arg2);
运行时使用 PyObject_Call。不可调用对象产生 Python TypeError,并映射为 TypePHP 可捕获的 Python 异常。
10.4 迭代
foreach ($pythonIterable as $value) {
// Python __iter__ / __next__
}
带 key 的形式:
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:
$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 边界时只传递原对象引用,不做内容复制。
推荐写法:
// 只转换一次,后续调用传递同一个 Python 对象。
use python\processor;
$pyItems = python\list($items);
for ($i = 0; $i < 1000; $i++) {
processor::consume($pyItems);
}
应避免在循环中反复把同一个 TypePHP 容器作为参数传入,因为每次跨越 Python 调用边界都会重新深拷贝:
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可能超过 TypePHPint范围。 - Python 类型的子类可能重载协议,不能按基础容器强制展开。
- 避免 phpy 当前“部分标量自动转换、部分对象保留包装”的行为进入 TypePHP 静态类型系统。
14.2 显式转换
Python 对象只有通过 toPlainValue()、python\scalar()(或手写等价的 PyCore::scalar())才能进入 TypePHP 类型规则:
$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 标准库operatormodule 的对应函数。/映射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。
例如:
$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 调用:
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 异常类型,暂定:
PyError
异常至少保留:
- Python exception type。
- message。
- Python traceback 对象。
- 格式化后的 traceback 字符串。
- 原始 Python exception instance。
示例:
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:
$values = python\list([1, 2, 3]);
$result = python\map(fn (int $value): int => $value * 2, $values);
Python 调用代理时:
- Python 参数按边界规则转换或包装为 TypePHP 值。
- 进入 ZendVM 调用 callable。
- 返回值转换为 Python 值。
- TypePHP 异常转换为 Python 异常。
闭包代理必须持有 Zend callable,防止 callable 在 Python 仍引用它时被释放。跨 VM 引用环必须由运行时显式检测或提供可预测的回收策略。
TypePHP callable 代理只是参数值,不是导出机制:只有 TypePHP 主动把代理传给 Python 后,Python 才能在该对象存活期间动态调用它。TypePHP 不生成可供 Python 独立导入的 module,也不注册全局函数或类。
18. phpy 生命周期与集成方式
TypePHP 复用 phpy 自己的 PHP 扩展入口和生命周期,不增加独立的 CPython bootstrap:
- phpy 的
MINIT初始化共享运行时、CPython 以及PyObject、PyDict等 Zend 类。 - phpy 的
RINIT建立本次请求需要的状态。 - TypePHP 程序在请求期间通过 phpy 注册到 ZendVM 的 internal classes、methods 和 object handlers 动态调用 Python。
- phpy 的
RSHUTDOWN释放请求级资源和代理。 - phpy 的
MSHUTDOWN在所有代理均已安全释放后关闭共享运行时和 CPython。
TypePHP 应通过与其他静态或动态链接 PHP 扩展相同的机制执行这些入口,不能重复初始化 CPython,也不能绕过 phpy 生命周期直接调用 Py_Initialize() 或 Py_Finalize()。
唯一产物是以 TypePHP 为入口的主程序或库。不会生成可被 CPython 导入的 .so / .pyd,不会向 Python 注册 TypePHP module、函数或类,也不存在 #[PythonExport]。
19. 性能原则
PyObject传参只增加必要的引用计数,不复制 Python 对象。pythonModuleMap只缓存已经绑定的PyModuleZend object zval,真实加载和全局 identity 直接复用 CPythonsys.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,顺序不可颠倒:
- 根据已确认的设计语义编写测试。
- 运行测试,确认它因为目标能力尚未实现或现有 BUG 而失败。
- 编写使该测试通过的最小实现。
- 运行相关测试和完整回归。
- 在测试保护下重构、清理和优化。
- 再次运行完整回归、内存检查和覆盖率检查。
禁止先完成实现,再补写只能验证当前实现细节的测试。每个 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,而仅声明未使用的 Pythonuse不报错。 - 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.modulesidentity 和重复 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. 已确认与待确认问题
已确认:
- Python 互调用是可选的扩展级特性;TypePHP 不链接或在编译期检查
libphpy.so,首次实际调用时若 phpy 未加载则由 Zend 抛出 PHPError。 - TypePHP 尽可能采用显式转换,不继承 phpy 的全部隐式转换行为。
- TypePHP 运算符在编译期改写为
operator::add($left, $right)一类 Python 标准库调用,不使用 phpy opcode handler,也不生成 phpy C++ 符号调用。 python根命名空间大小写不敏感,其后的所有 Python 符号大小写敏感。python是编译器处理的特殊语言命名空间。- 运行时类继续使用
PyObject、PyDict等 phpy 公开名称。 python\dict()等构造语法是现有 phpy 类构造器的语法糖;python\print()等是PyCoreAPI 的语法糖。new PyList()与python\list()具有相同的PyListtyped object 类型和优化能力。- phpy 解决运行时问题,TypePHP 只通过缓存的
zend_function*和 PHPX/Zend 通用对象 API 动态调用 phpy Facade。 - TypePHP 的 Python 专用实现与测试放入独立子目录,通过受控入口接入通用编译流程。
- Python 线程、
asyncio和 subinterpreter 永久禁止,且不作为后续兼容目标。 ===/!==分别映射 Python identity 的is/is not;==/!=使用 Python 值比较。- 仅支持 TypePHP 主动调用 Python;不生成 Python extension,不提供
#[PythonExport],不向 Python 注册 TypePHP 符号。 - CPython 和 bridge 生命周期完全复用 phpy 的
MINIT/RINIT/RSHUTDOWN/MSHUTDOWN入口。 - Python 包变量使用
math::$pi形式读取;math::pi保持 PHP 常量访问含义,不被重新解释。 np::array()表示读取并调用 Python 包成员;该成员可以是函数、class 或其他 callable,具体类型由 Python 运行时决定。PyObject可以与 TypePHP 值混合运算;TypePHP 操作数转换为 Python 对象后,整个运算由 CPython protocol 执行,结果保持为PyObject。- Python 函数、方法、class 构造和 builtin 调用的结果一律保持为
PyObject或已知的 phpy 子类;禁用 phpy 返回值隐式转换。 PyObject::toPlainValue()是推荐的链式显式转换关键词;python\scalar()保留为等价入口。两者退出 Python 类型规则后均可继续使用普通 TypePHP 转换,例如$obj->toPlainValue()->toInt()。- TypePHP 调用 Python 时,所有参数自动转换为 Python 类型;TypePHP 数组递归深拷贝,空数组默认转换为 Python list。
- 性能敏感代码应复用
PyDict、PyList、PyStr等代理对象,避免同一 TypePHP 值反复转换和深拷贝。 - TypePHP 的主要语言增量是
use python\...和模块别名;使用别名时通过与funcMap同类的 lazy indexed map 调用 phpy import,其他运行时能力优先直接复用 phpy。 use python\module只登记 namespace 标记;当前.php文件没有使用该别名的任何符号时,不生成 helper,也不执行运行时 import。- 发现
module::$attr或module::func()时,才为完整 module 名称分配 ID;未使用的use不占 map slot,也不执行 import。 pythonModuleMap与funcMap一样集中声明、按 ID lazy lookup;区别是 module 保存为拥有引用的 Zend object zval,必须在 request clean 中逐项zval_ptr_dtor()并恢复为UNDEF。sys.modules负责全局加载状态和 identity,pythonModuleMap只表示 TypePHP 已经完成的 module alias binding。- TypePHP 生成代码只依赖 PHPX/ZendVM;
PyCore::import()、builtin、对象方法和转换均解析为zend_function*动态调用。 - Python 运算符隐式使用
python\operatormodule;完整运算协议由 CPythonoperator函数处理,不直接调用 dunder,也不由 TypePHP 实现 reflected fallback。
仍待确认:
- 初版是否需要模块属性赋值?如果需要,是否使用
module::$name = $value?
该待确认项不阻塞只读 module binding 等已确认阶段的实施;属性写入必须在语义确认并先补测试后才能实现。