Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ php webman typephp:doctor
# 默认输出到 dist/
php webman typephp:package

# SaiAdmin:自动发现核心、app、support 与已安装插件服务端业务代码
php webman typephp:package --profile=saiadmin

# dist/ 已存在时,显式确认覆盖
php webman typephp:package --force

Expand All @@ -67,6 +70,9 @@ php webman typephp:package --refresh-main

默认 builder 为 `tinywan/typephp-webman-builder:v0.1.3`。编译在 Docker 中完成,宿主机不需要 C++、Clang 或 TypePHP 编译器。

SaiAdmin 的支持矩阵、开发规范、存量迁移、配置样例、验收脚本和实跑证据见
[`docs/saiadmin-aot/`](docs/saiadmin-aot/README.md)。

### 4. 启动产物

将 `dist/` 复制到兼容的 Linux x86_64/glibc 服务器,在目录内启动:
Expand Down Expand Up @@ -106,6 +112,7 @@ dist/
├── lib/ # 随包发布的底层系统与扩展动态依赖库 (ldd 完整收集)
├── runtime/ # 运行时缓存与日志目录 (logs, views)
├── build-manifest.json # 输入、镜像与时间等构建元数据
├── source-coverage.json # SaiAdmin profile 的逐业务文件 AOT 覆盖清单
├── config/ # 项目运行时配置(若存在)
├── public/ # 静态资源(若存在)
└── app/view/ # 视图模板(若存在)
Expand All @@ -118,6 +125,7 @@ dist/
| 命令 | 说明 |
| --- | --- |
| `php webman typephp:package` | 使用默认 builder 构建 Linux portable-dir |
| `php webman typephp:package --profile=saiadmin` | 使用锁版本、失败关闭的 SaiAdmin 自动发现与兼容规则构建 |
| `php webman typephp:package --force` | 覆盖已有输出,并保留旧目录备份 |
| `php webman typephp:package --refresh-main` | 强制从最新官方 stub 刷新 `main.php`(旧文件自动备份) |
| `php webman typephp:package --image=...` | 使用指定且经过验证的 Docker 镜像 |
Expand Down Expand Up @@ -145,6 +153,12 @@ return [

`--image` 的优先级最高;未指定时使用上述 `docker.image`,配置缺失时才回退至 `tinywan/typephp-webman-builder:v0.1.3`。

### SaiAdmin profile

`--profile=saiadmin` 要求存在 `plugin/saiadmin` 和有效的 `composer.lock`。当前首个支持矩阵固定为 SaiAdmin `6.1.1`、ThinkORM `v3.0.34`、Carbon `3.13.2`;未知版本会在进入 Docker 编译前失败。

profile 会自动发现根 `app/`、`support/`、SaiAdmin 核心和每个 `plugin/*/app/`,并编译完整 Composer 依赖树。兼容副本只写入 `.typephp/build/`,普通 PHP 源码不变。构建输出中的 `source-coverage.json` 逐项记录业务 PHP 是直接编译还是由哪个 AOT 副本替代;业务文件未分类、被排除却没有等价副本,或漂移到未知依赖版本时都会终止构建。

## 🎯 可信 MVP 边界

当前第一阶段只承诺已经验证的组合:
Expand Down
26 changes: 12 additions & 14 deletions docker/AssignOpTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -287,20 +287,18 @@ protected function parseAssignToList(Expr $left, Expr $right): string
continue;
}
if ($item instanceof ArrayItem) {
$key = $item->key ? $this->parseArrayKey($item->key) : (string) $k;
$value = new Expr\ArrayDimFetch(
new Variable($tmpVar),
$item->key ?? new Node\Scalar\Int_($k),
);
if ($item->value instanceof Expr\List_) {
$nestedTmp = $this->genTmpVarName();
$this->addLocalVar($nestedTmp, Type::ARRAY);
$code .= $this->getIndent() . "{$nestedTmp} = {$tmpVar}.item({$key});" . PHP_EOL;
$code .= $this->getIndent()
. $this->parseAssignToList($item->value, new Variable($nestedTmp))
. $this->parseAssignToList($item->value, $value)
. PHP_EOL;
} else {
$var = $this->parseWritableIdentifier($item->value);
if ($this->isVarExpr($item->value) and !$this->hasVar($var)) {
$this->addLocalVar($var, Type::VAR);
}
$code .= $this->getIndent() . "{$var} = {$tmpVar}.item({$key});" . PHP_EOL;
$code .= $this->getIndent()
. $this->parseAssignFinally($item->value, $value)
. ';' . PHP_EOL;
}
} else {
$this->unsupportedSyntax($item);
Expand Down Expand Up @@ -885,7 +883,7 @@ protected function parseAssignOp(Expr\AssignOp $node, string $op): string
return $pythonOperator;
}
$propertyWriteTarget = $this->preparePropertyWriteTarget($node->var);
$this->guardLiteralDivisionByZero($node->expr, $op);
$this->guardLiteralDivisionByZero($node->var, $node->expr, $op);

// A compound division/modulo on a NATIVE scalar slot with a proven
// zero divisor cannot fall through to the raw C++ operator (SIGFPE
Expand All @@ -894,7 +892,7 @@ protected function parseAssignOp(Expr\AssignOp $node, string $op): string
// lower the whole expression to the PHP-semantics binary operation
// and leave the target untouched.
if (($op === '/=' || $op === '%=')
&& !$this->nativeTypes
&& $this->varIntTypes
&& $this->isZeroLiteral($node->expr)
&& $this->isVarExpr($node->var)
&& $this->hasVar((string) $this->parseIdentifier($node->var))
Expand All @@ -903,7 +901,7 @@ protected function parseAssignOp(Expr\AssignOp $node, string $op): string
// std::int()/std::float() values are an explicit opt-in to native
// C++ arithmetic; changing them to PHP semantics here would be as
// wrong as the undefined raw operation. Keep the compile-time
// rejection native_types mode uses.
// rejection varint_types mode uses.
if ($this->isExplicitNativeArithmeticExpr($node->var)) {
$this->fatalError($node->expr, 'Cannot divide or modulo by zero');
}
Expand Down Expand Up @@ -1201,7 +1199,7 @@ protected function parseNativePropertyAssignOp(Expr\AssignOp $node, string $op):
// A direct zend_long reference would bypass that behavior completely.
// Native objects cannot cross the Variant boundary and retain their
// native C++ property access path.
if (!$this->nativeTypes
if ($this->varIntTypes
&& $def->type === Type::INT
&& !$this->isNativeObjectClass($this->detectClassOfExpr($node->var->var))
&& in_array($op, ['+=', '-=', '*=', '/=', '%=', '**=', '<<=', '>>=', '&=', '|=', '^='], true)
Expand Down
158 changes: 140 additions & 18 deletions docker/CompilerBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,83 @@ class CompilerBase implements PropertyAccessContext
use LoopVarOptimizer;
use SsaPropOptimizer;

/**
* Dynamic reference slots can contain Throwable objects. Defer their
* validation to throwValue(), just like ordinary dynamic variables.
*/
protected function parseThrow(mixed $expr): string
{
if ($this->method === '__destruct') {
$this->warning($expr, "Throwing exception in {$this->getFullClassName()}::__destruct() may cause memory leak");
}
$class = $this->detectDeclaredClassOfExpr($expr->expr);
if ($this->isNativeObjectClass($class)) {
$this->fatalError($expr, 'Native objects cannot be thrown as Zend exceptions');
}
$type = $this->detectTypeOfExpr($expr->expr);
if ($this->isNewExpr($expr->expr)) {
$ex = $this->parseExpr($expr->expr);
return 'php::throwException(' . $ex . ')';
}
if ($this->isVarExpr($expr->expr)) {
$ex = $this->parseIdentifier($expr->expr);
if ($type === Type::OBJECT) {
return 'php::throwException(' . $ex . ')';
}
} else {
$ex = $this->parseExpr($expr->expr);
}
if (!in_array($type, [Type::VAR, Type::REF, Type::OBJECT], true) && $class === '') {
$this->fatalError($expr, 'Can only throw objects');
}
return 'php::throwValue(' . $ex . ')';
}

/**
* Parameterized toArray() methods on ordinary PHP classes are application
* APIs, not TypePHP conversion hooks (for example JsonResource::toArray(Request)).
*/
protected function assertKeywordConversionMethodSignature(
NodeAbstract $node,
string $class,
string $method,
FunctionDef $function,
string $expectedType,
bool $nativeClass,
): void {
$kind = $nativeClass ? 'Native conversion method' : 'Conversion method';
if ($function->argInfoList !== []) {
if (!$nativeClass && strtolower($method) === 'toarray') {
return;
}
$this->fatalError($node, "{$kind} `{$class}::{$method}()` must not accept arguments");
}
$hasExactReturnType = $function->returnType === $expectedType;
if ($expectedType === Type::VAR) {
$hasExactReturnType = in_array(strtolower($function->returnTypeStr), ['mixed', 'any'], true);
}
if ($function->returnsByRef || $function->returnNullable || !$hasExactReturnType) {
$expectedTypeName = match ($expectedType) {
Type::INT => 'int',
Type::FLOAT => 'float',
Type::STR => 'string',
Type::BOOL => 'bool',
Type::ARRAY => 'array',
Type::STREAM => 'Stream',
Type::BIGINT => 'BigInt',
Type::BIGFLOAT => 'BigFloat',
Type::DECIMAL => 'Decimal',
Type::OBJECT => 'object',
Type::VAR => 'mixed` or `any',
default => $expectedType,
};
$this->fatalError(
$node,
"{$kind} `{$class}::{$method}()` must return exactly `{$expectedTypeName}`",
);
}
}

public const string DEFAULT_PHP_VERSION = '8.5';
protected const string NATIVE_PROPERTY_VALUE_VAR = 'var';
protected const string NATIVE_PROPERTY_VALUE_DYNAMIC = 'dynamic';
Expand Down Expand Up @@ -490,7 +567,7 @@ protected function getBoolValue(Expr\ConstFetch $expr): string
protected array $nativeClassDeclarations = [];
/** @var array<string, true> Request-reset initialization flags for Native static locals. */
protected array $nativeStaticInitializers = [];
protected bool $nativeTypes = false;
protected bool $varIntTypes = false;
protected bool $decimalTypes = false;
protected bool $bigintTypes = false;
protected string $rootPath;
Expand Down Expand Up @@ -997,7 +1074,7 @@ protected function removeCommonPrefix(string $short, string $long): string
return $this->getPlatform()->removeCommonPrefix($short, $long);
}

protected function getVarType(string $name): string
protected function getRawVarType(string $name): string
{
if ($this->hasLocalVar($name)) {
return $this->context->localVars[$name];
Expand All @@ -1009,6 +1086,16 @@ protected function getVarType(string $name): string
return Type::VAR;
}

/**
* Return the value type visible to expressions. A native C++ reference has
* the same operators and assignment rules as its referenced value; only
* ABI/binding code should inspect getRawVarType().
*/
protected function getVarType(string $name): string
{
return Type::getReferencedType($this->getRawVarType($name));
}

/**
* Resolve the ClassDef for an object expression (variable or $this).
*/
Expand Down Expand Up @@ -1071,7 +1158,7 @@ protected function resetClass(): void
protected function resetFile(): void
{
$this->indentLevel = 0;
$this->nativeTypes = false;
$this->varIntTypes = false;
$this->decimalTypes = false;
$this->bigintTypes = false;
$this->classesDefineInFile = [];
Expand Down Expand Up @@ -1348,6 +1435,21 @@ protected function getFunctionCallCache(): string
return 'typephp_get_function_call_cache(FunctionCallCacheId{' . $id . '})';
}

/** Return the function-local late-static-bound class entry. */
protected function getCalledCeExpr(): string
{
$this->context->needsCalledCe = true;
return '_typephp_called_ce';
}

/** Return the function-local late-static-bound class name. */
protected function getCalledClassExpr(): string
{
$this->context->needsCalledCe = true;
$this->context->needsCalledClass = true;
return '_typephp_called_class';
}

protected function getClassEntryPtr(string $className): string
{
$id = $this->getClassId($className);
Expand Down Expand Up @@ -2499,9 +2601,9 @@ protected function parseReturn(Node\Stmt\Return_ $v): string
// runtime overflow promotes the result to float. Keep the Variant
// representation through the return boundary so a declared scalar
// return type observes and rejects that float exactly as PHP does.
// `use native_types` intentionally opts into native C++ arithmetic
// `use varint_types` intentionally opts into native C++ arithmetic
// semantics and is therefore excluded from this check.
if (!$this->nativeTypes && $type === Type::INT && $this->exprCanOverflowInt($v->expr)) {
if ($this->varIntTypes && $type === Type::INT && $this->exprCanOverflowInt($v->expr)) {
$type = Type::VAR;
}
$nativeExpressionClass = $this->detectClassOfExpr($v->expr);
Expand Down Expand Up @@ -2831,9 +2933,21 @@ protected function getNativeMethod(CallLike $expr, string $class, string $method
}
if (!$this->hasClass($classDef->extends)) {
if ($classDef->inheritedFromInternalClass) {
if (!Reflection::hasMethod($classDef->extends, $method) and !Reflection::hasMethod($classDef->extends, $method . '__call')) {
$lateStaticCall = $expr instanceof Expr\StaticCall
&& $this->isNameExpr($expr->class)
&& strtolower($expr->class->toString()) === 'static';
if ($lateStaticCall && !$this->isCurrentClassFinal()) {
return false;
}
$magicMethod = $expr instanceof Expr\StaticCall ? '__callStatic' : '__call';
if ($classDef->hasMethod($magicMethod)) {
return false;
}
if (!Reflection::hasMethod($classDef->extends, $method)
&& !Reflection::hasMethod($classDef->extends, $magicMethod)
) {
$this->fatalError($expr, 'Class `' . $classDef->getNamespacedName() . '` inherits from a internal class, but the class `' .
$classDef->extends . '` does not have a `' . $method . '` method or a `__call` magic method');
$classDef->extends . '` does not have a `' . $method . '` method or a `' . $magicMethod . '` magic method');
} else {
$this->climate->cyan('Dynamically calling internal class method `' . $classDef->extends . '::' . $method . '()`');
throw new DynamicCall();
Expand Down Expand Up @@ -3015,7 +3129,7 @@ protected function detectTypeOfExpr($expr): string
case 'Expr_UnaryPlus':
$innerType = $this->detectTypeOfExpr($expr->expr);
if (
!$this->nativeTypes
$this->varIntTypes
&& $exprType === 'Expr_UnaryMinus'
&& $innerType === Type::INT
&& $this->constantIntValue($expr->expr) === PHP_INT_MIN
Expand Down Expand Up @@ -3103,7 +3217,7 @@ protected function detectTypeOfExpr($expr): string
if ($leftType === Type::FLOAT || $rightType === Type::FLOAT) {
return Type::FLOAT;
}
if (!$this->nativeTypes && $leftType === Type::INT && $rightType === Type::INT) {
if ($this->varIntTypes && $leftType === Type::INT && $rightType === Type::INT) {
$op = match ($exprType) {
'Expr_BinaryOp_Plus' => '+',
'Expr_BinaryOp_Minus' => '-',
Expand Down Expand Up @@ -3936,7 +4050,7 @@ protected function parseNew(Expr\New_ $expr): string
if ($this->classDef?->nativeObject) {
$this->fatalError($expr, 'Native classes do not support `new static()`');
}
$cePtr = Symbol::getCalledCe();
$cePtr = $this->getCalledCeExpr();
} else {
if ($className === 'self') {
$className = $this->getFullClassName();
Expand Down Expand Up @@ -4134,7 +4248,7 @@ protected function resolveInstanceofClassPtr(NodeAbstract $class): string
if (!$this->classDef) {
$this->fatalError($class, 'Cannot use "static" outside a class');
}
return Symbol::getCalledCe();
return $this->getCalledCeExpr();
} else {
$className = $this->getNamespacedClassName($className);
}
Expand Down Expand Up @@ -5217,6 +5331,16 @@ protected function genLocalVarDecl(array $localVars): string
protected function genScopeVarDecl(): string
{
$code = '';
if ($this->context->needsCalledCe) {
$code .= $this->getIndent()
. 'zend_class_entry *const _typephp_called_ce = typephp_get_called_ce(this_);'
. PHP_EOL;
}
if ($this->context->needsCalledClass) {
$code .= $this->getIndent()
. 'php::Str const _typephp_called_class = typephp_get_called_class(_typephp_called_ce);'
. PHP_EOL;
}
if ($this->context->hasMultiLevelBreak) {
$code .= $this->getIndent() . 'int _brk_flag = 0;' . PHP_EOL;
}
Expand Down Expand Up @@ -5276,13 +5400,11 @@ protected function genScopeVarDecl(): string
$code .= $this->getIndent() . $info['type'] . ' &' . $name . ' = ' . $zvalMacro . '(' . $info['getter'] . '.unwrap_ptr());' . PHP_EOL;
}
}
foreach ($this->context->staticPropRefs as $name => $info) {
$getter = Symbol::getStaticProperty() . '(' . $info['classPtr'] . ', ' . $info['offsetExpr'] . ')';
if (($info['kind'] ?? 'zval') === 'var') {
$code .= $this->getIndent() . Type::VAR . ' ' . $name . ' = ' . $getter . ';' . PHP_EOL;
} else {
$code .= $this->getIndent() . 'zval *' . $name . ' = ' . $getter . '.unwrap_ptr();' . PHP_EOL;
}
foreach ($this->context->staticPropRefs as $info) {
$code .= $this->getIndent() . 'zval *' . $info['name'] . ' = nullptr;' . PHP_EOL;
$code .= $this->getIndent() . 'const auto ' . $info['accessorName'] . ' = [&]() {'
. ' return typephp_get_static_property_cached(' . $info['name'] . ', [&]() {'
. ' return ' . $info['resolver'] . '; }); };' . PHP_EOL;
}
return $code;
}
Expand Down
Loading