From 4b1b20c7ca18b417cef05f2773c7eb1b96eb2592 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 30 Jun 2026 19:58:04 +0800 Subject: [PATCH] =?UTF-8?q?refactor(php):=20=E9=87=8D=E6=9E=84=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E8=AE=BF=E9=97=AE=E8=A7=A3=E6=9E=90=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E5=B9=B6=E5=BC=95=E5=85=A5=E7=BC=96=E8=AF=91=E9=98=B6=E6=AE=B5?= =?UTF-8?q?=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入 PropertyAccessResolver 类统一处理属性访问逻辑 - 添加编译阶段管理 (idle/prepare/convert) 确保模块正确使用时机 - 将 isSameClassName、isSameOrSubclassOf、canAccessProtectedProperty 方法委托给 resolver - 实现 PropertyAccessResult DTO 承载属性访问解析结果 - 为 Preprocessor 和 Translator 添加编译阶段进入/恢复逻辑 - 创建 CompilerPhaseTest 验证阶段管理功能 - 更新文档说明重构计划和模块拆分方向 - 修改函数返回值测试以适配重构后的测试需求 --- docs/README.md | 13 + docs/REFACTORING_PLAN.md | 396 ++++++++++++++++++ phpunit/src/CompilerPhaseTest.php | 61 +++ src/Php/CompilerBase.php | 99 ++--- src/Php/Preprocessor.php | 113 ++--- src/Php/Resolver/PropertyAccessResolver.php | 109 +++++ src/Php/Resolver/PropertyAccessResult.php | 24 ++ src/Php/Translator.php | 33 +- .../closure/read-private-prop-in-closure.phpt | 35 ++ tests/aot/functions/return-value.phpt | 8 +- 10 files changed, 765 insertions(+), 126 deletions(-) create mode 100644 docs/REFACTORING_PLAN.md create mode 100644 phpunit/src/CompilerPhaseTest.php create mode 100644 src/Php/Resolver/PropertyAccessResolver.php create mode 100644 src/Php/Resolver/PropertyAccessResult.php create mode 100644 tests/aot/closure/read-private-prop-in-closure.phpt diff --git a/docs/README.md b/docs/README.md index e6bd8505..2a6856c8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -119,6 +119,19 @@ --- +### 9. [核心重构计划](REFACTORING_PLAN.md) +**必读指数**: ⭐⭐⭐⭐ + +内容概要: +- 核心类职责拆分方向 +- TypeSystem、SymbolResolver、PropertyAccessResolver、CallResolver 等模块规划 +- 渐进式重构阶段计划 +- 测试门禁和风险控制要求 + +**适合人群**: 核心开发者、架构重构参与者 + +--- + ## 🎯 快速导航 ### 按使用场景 diff --git a/docs/REFACTORING_PLAN.md b/docs/REFACTORING_PLAN.md new file mode 100644 index 00000000..18d5095c --- /dev/null +++ b/docs/REFACTORING_PLAN.md @@ -0,0 +1,396 @@ +# AOT 编译器核心重构计划 + +## 背景 + +当前 AOT 编译器核心类承担了过多职责,尤其是 `CompilerBase`、`Translator` 等类同时包含 AST 分发、类型推导、属性访问解析、调用解析、代码生成、诊断信息、上下文状态维护等逻辑。随着功能持续增加,这种结构会带来以下问题: + +- 封装性不足,修改一个语义点时容易影响多个代码路径。 +- 代码复用不足,同类逻辑在普通属性、静态属性、nullsafe、assignment、isset/empty/refval 等路径中重复实现。 +- 编译期检查容易出现绕过路径,例如某些动态 fallback 没有复用静态 resolver。 +- 单个类代码量过大,review、测试定位和长期维护成本持续升高。 +- 设计边界不清晰,类型系统、符号解析、属性访问、调用生成之间耦合过深。 + +本计划用于指导后续逐步重构,目标是改善架构质量,同时避免一次性大规模重写带来的行为回归风险。 + +## 核心原则 + +1. 渐进式重构,禁止一次性重写核心编译流程。 +2. 优先抽离无状态或低状态依赖的纯逻辑,再抽离依赖编译上下文的逻辑。 +3. 每一步重构应尽量保持行为不变,行为变更必须单独说明并补测试。 +4. 新模块应围绕稳定领域建模,而不是简单按 AST 节点机械拆分。 +5. 优先使用小型 service/helper、明确 DTO、resolver、emitter;仅在边界稳定后再引入 Visitor、Strategy 等模式。 +6. 所有错误信息保持 PHP 风格,不暴露实现细节,不使用 “AOT 禁止” 这类表述。 +7. 每个阶段必须有 phpunit 或 phpt 回归验证,尤其是属性、类型、调用、继承、异常等高风险路径。 + +## 目标架构方向 + +### CompilerBase + +最终应收敛为编译上下文、公共工具、AST 调度入口和跨模块协作层,不再直接承载大量领域逻辑。 + +保留职责: + +- 当前文件、函数、类、方法上下文管理。 +- 临时变量、局部变量、作用域状态维护。 +- AST 顶层分发入口。 +- fatal/warning 的统一入口。 +- 与下层 resolver/emitter 的协作。 + +逐步移出职责: + +- 类型声明解析和类型兼容判断。 +- 属性可见性、属性 offset、typed property 检查。 +- 函数和方法调用解析。 +- 复杂表达式代码生成。 +- union/intersection/nullable runtime typecheck 生成。 + +### Translator + +保留文件级、类级、函数级编译流程控制,逐步减少具体语义检查和表达式生成逻辑。 + +保留职责: + +- 文件扫描和转换入口。 +- 类、函数、方法代码块生成流程。 +- class/function/constant/property metadata 注册。 +- 编译产物组织。 + +逐步移出职责: + +- 继承兼容检查的细节逻辑。 +- trait 属性冲突检查细节。 +- 参数、返回值、属性类型兼容判断。 + +## 模块拆分计划 + +### 1. TypeSystem + +职责: + +- 类型声明解析。 +- PHP 类型到 AOT 内部类型映射。 +- nullable、union、intersection 类型展开。 +- 参数、返回值、属性、常量的类型兼容判断。 +- 静态类型与 runtime typecheck 的边界定义。 +- 类型字符串格式化。 + +建议子模块: + +- `TypeResolver` +- `TypeCompatibility` +- `TypeCheckEmitter` +- `TypeStringFormatter` + +设计要求: + +- union、intersection、nullable 在静态阶段仍可按 mixed/any 处理,但必须保留 runtime typecheck 信息。 +- `self`、`parent`、`static` 等特殊类型名的解析规则必须集中,避免多个路径实现不一致。 +- 类型错误信息必须包含函数、方法、参数、属性等必要上下文。 + +### 2. SymbolResolver + +职责: + +- 命名空间解析。 +- use alias 解析。 +- `self`、`parent`、`static` 解析。 +- 类、接口、trait、函数、常量名称规范化。 +- 动态符号和静态可解析符号的边界判断。 + +建议接口: + +- `resolveClassName(NodeAbstract $node): SymbolResolution` +- `resolveFunctionName(NodeAbstract $node): SymbolResolution` +- `resolveMethodScope(NodeAbstract $node): SymbolResolution` +- `resolveClassConstScope(NodeAbstract $node): SymbolResolution` + +设计要求: + +- 不同调用路径不能重复实现 `self/parent/static` 规则。 +- 对无法静态确定的符号,应明确返回 dynamic 状态,而不是默默退化为字符串拼接。 + +### 3. PropertyAccessResolver + +职责: + +- 对象属性访问解析。 +- 静态属性访问解析。 +- nullsafe 属性访问静态检查。 +- private/protected/public 可见性检查。 +- native property offset 查找。 +- typed property 写入 typecheck 信息生成入口。 +- static-vs-instance 属性误用检查。 + +建议接口: + +- `resolveInstancePropertyAccess(PropertyAccessRequest $request): PropertyAccessResult` +- `resolveStaticPropertyAccess(StaticPropertyAccessRequest $request): PropertyAccessResult` +- `assertReadable(PropertyAccessResult $result): void` +- `assertWritable(PropertyAccessResult $result): void` +- `emitRead(PropertyAccessResult $result): string` +- `emitWrite(PropertyAccessResult $result, string $value): string` + +需要统一覆盖的路径: + +- `$obj->prop` +- `$obj?->prop` +- `Class::$prop` +- `self::$prop` +- `parent::$prop` +- `static::$prop` +- `isset($obj->prop)` +- `empty($obj->prop)` +- `refval($obj->prop)` +- 普通赋值、复合赋值、自增自减、unset。 + +第一优先级建议从本模块开始,因为近期问题集中在属性访问和可见性绕过,测试边界相对清晰。 + +### 4. CallResolver + +职责: + +- 函数调用解析。 +- 对象方法调用解析。 +- 静态方法调用解析。 +- native call 与 dynamic call 选择。 +- named args、unpack、by-ref 参数处理。 +- closure 和动态 callable 退化规则。 + +建议接口: + +- `resolveFunctionCall(CallRequest $request): CallResolution` +- `resolveMethodCall(MethodCallRequest $request): CallResolution` +- `resolveStaticCall(StaticCallRequest $request): CallResolution` +- `emitCall(CallResolution $resolution): string` + +设计要求: + +- 静态 function 和内置 function 参数信息明确时,可以自动转引用。 +- 动态调用、closure、编译期无法获取参数 by-ref 信息时,必须要求显式 `refval()`。 +- 使用 unpack 并追加尾部 named args 时,应退化为 dynamic call,不能走 native call。 + +### 5. ExpressionEmitter + +职责: + +- 表达式级代码生成。 +- 将大型 `parseExpr()` 分发逻辑逐步拆分。 +- 复用 resolver 结果生成 C++ 代码。 + +建议按领域拆分,而不是一开始拆成大量 AST visitor: + +- `AssignmentEmitter` +- `PropertyEmitter` +- `CallEmitter` +- `ArrayEmitter` +- `ControlExprEmitter` +- `ObjectEmitter` + +设计要求: + +- 先保持现有 `parseExpr()` 作为调度入口。 +- 每次只迁移一组表达式,迁移后跑对应测试组。 +- 对表达式副作用、求值顺序、临时变量生成必须保守处理。 + +### 6. Diagnostic + +职责: + +- 统一 fatal/warning 构造。 +- 提供上下文增强能力。 +- 保证错误消息风格接近 PHP。 + +建议能力: + +- 当前函数/方法名。 +- 参数名。 +- 属性名和类名。 +- 源码位置。 +- 声明位置与使用位置。 + +设计要求: + +- 错误信息不出现 “AOT” 作为行为主体。 +- 对用户可修复的问题,应包含准确符号名。 +- 编译期能发现的问题优先编译期 fatal,不应依赖运行时 typecheck 异常兜底。 + +## 阶段计划 + +### 阶段 1:属性访问解析模块化 + +目标: + +- 抽离 `findNativeProperty()`、`findNativeStaticProperty()`、`canAccessProtectedProperty()` 等属性访问解析逻辑。 +- 保持生成代码基本不变。 +- 建立统一的 `PropertyAccessResult`,承载属性声明、声明类、访问类、是否 native、offset、是否 dynamic 等信息。 + +范围: + +- 普通对象属性读取。 +- 静态属性读取。 +- nullsafe 属性静态检查。 +- 属性可见性检查。 + +当前进展: + +- 已建立 `PropertyAccessResolver` 和 `PropertyAccessResult` 作为属性访问解析的第一层抽象。 +- `CompilerBase::findNativeProperty()` 保留为兼容入口,并委托 resolver 执行 native property 查找、static-vs-instance 检查和可见性检查。 +- `CompilerBase::isSameClassName()`、`isSameOrSubclassOf()`、`canAccessProtectedProperty()` 已委托 resolver,避免规则继续扩散。 +- 已添加 `prepare/convert/idle` 编译阶段状态;`PropertyAccessResolver` 只能在 convert 阶段创建和使用,避免预处理阶段误用不完整的类表状态。 +- 当前迁移保持生成代码不变,后续阶段再统一 read/write emitter。 + +验证: + +- `phpunit/src/NativePropertyTest.php` +- `phpunit/src/InheritanceErrorTest.php` +- object property 相关 phpt。 +- static property 相关 phpt。 +- nullsafe 相关 phpt。 + +### 阶段 2:属性写入路径统一 + +目标: + +- 所有属性写入路径先 resolve,再 emit。 +- typed property runtime typecheck 从散落逻辑收敛到属性写入模块。 +- 消除 assignment、compound assignment、inc/dec、unset 各自实现属性访问规则的问题。 + +范围: + +- `$obj->prop = $value` +- `$obj->prop += $value` +- `$obj->prop++` +- `unset($obj->prop)` +- `Class::$prop = $value` +- `??=` 相关属性路径。 + +验证: + +- typed property 相关 phpunit/phpt。 +- object property optimization 相关 phpt。 +- nullsafe 写上下文错误测试。 +- private/protected/static 属性错误测试。 + +### 阶段 3:类型系统模块化 + +目标: + +- 抽离类型声明解析、类型兼容、runtime typecheck 生成。 +- 明确静态类型推导和运行时 typecheck 的职责边界。 +- 减少参数、返回值、属性、常量各自重复处理类型规则。 + +范围: + +- `parseTypeDecl()`。 +- `buildTypeCheckFromNode()`。 +- 参数 typecheck。 +- 返回值 typecheck。 +- 属性 typecheck。 +- 常量类型处理。 + +验证: + +- union、intersection、nullable 类型测试。 +- 参数、返回值、属性 typecheck 测试。 +- namespace constant 测试。 +- constructor、void expression 相关测试。 + +### 阶段 4:调用解析模块化 + +目标: + +- 统一函数、方法、静态方法调用解析。 +- 明确 native call、dynamic call、closure call 的退化规则。 +- 集中处理 named args、unpack、by-ref 参数。 + +范围: + +- `parseFuncCall()`。 +- `parseMethodCall()`。 +- `parseStaticCall()`。 +- call args 解析。 +- native/internal/user function 调用路径。 + +验证: + +- named args、unpack 相关 phpt。 +- by-ref 参数相关 phpt。 +- closure 相关 phpt。 +- parent/self/static call 相关 phpt。 + +### 阶段 5:表达式生成器拆分 + +目标: + +- 将 `parseExpr()` 背后的大量表达式生成逻辑迁移到领域 emitter。 +- `CompilerBase` 保持调度和共享上下文能力。 +- 降低单文件和单类代码量。 + +范围: + +- AssignmentEmitter。 +- PropertyEmitter。 +- CallEmitter。 +- ArrayEmitter。 +- ControlExprEmitter。 + +验证: + +- 每迁移一个 emitter,跑对应测试组。 +- 最后跑核心 phpunit 和选定 phpt 回归集。 + +### 阶段 6:Translator 收敛 + +目标: + +- 将继承兼容、trait 合并、类成员校验等逻辑抽离成专门 checker。 +- `Translator` 聚焦编译流程组织。 + +建议子模块: + +- `InheritanceChecker` +- `TraitCompositionChecker` +- `ClassMemberValidator` +- `FunctionSignatureChecker` + +验证: + +- 继承错误 phpunit。 +- trait 相关 phpt。 +- interface/abstract/final/readonly 相关 phpt。 + +## 测试门禁 + +每个重构 PR 或阶段至少满足: + +- 相关 phpunit 必须通过。 +- 相关 phpt 必须通过。 +- 如果修改 C++/phpx,需要补 gtest 并通过对应测试。 +- 如果引入新编译期错误,需要新增固定 fixture 到 `phpunit/code` 或新增 phpt。 +- 不使用 `file_put_contents()` 临时生成源码作为新测试方式。 + +建议按模块维护最小回归集: + +- 属性模块:`NativePropertyTest`、`InheritanceErrorTest`、object property、static property、nullsafe。 +- 类型模块:type_decl、type_hits、typed property、union/intersection/nullable。 +- 调用模块:function call、method call、parent_call、closure、named args、unpack、by-ref。 +- 控制流和表达式:ternary、match、goto、loop、array、coalesce、void expression。 + +## 风险控制 + +- 不在同一个变更中同时做大规模文件移动和行为变更。 +- 每次迁移前先补当前行为的测试,尤其是历史 bug 对应路径。 +- 保留旧入口一段时间,通过 adapter 调用新模块,降低切换风险。 +- 对动态 PHP 语义保持保守,无法静态确定时不要过度优化。 +- 对 AOT 明确不兼容 PHP 历史包袱的地方,应在文档和错误信息中表达为语言规则,而不是实现限制。 + +## 推荐下一步 + +从 `PropertyAccessResolver` 开始。 + +理由: + +- 最近 bug 多集中在属性访问、可见性、typed property、nullsafe、static-vs-instance 路径。 +- 现有测试较容易扩展。 +- 属性访问是类型系统、优化器、调用生成之外相对独立的领域,适合先建立 resolver/result 模式。 +- 完成后可直接减少 `CompilerBase` 中的复杂分支,并为后续 ExpressionEmitter 拆分打基础。 diff --git a/phpunit/src/CompilerPhaseTest.php b/phpunit/src/CompilerPhaseTest.php new file mode 100644 index 00000000..b767aa61 --- /dev/null +++ b/phpunit/src/CompilerPhaseTest.php @@ -0,0 +1,61 @@ +forTest = true; + return $compiler; + } + + public function enterPreparePhase(): void + { + $this->enterCompilerPhase(self::PHASE_PREPARE); + } + + public function enterConvertPhase(): void + { + $this->enterCompilerPhase(self::PHASE_CONVERT); + } + + public function probePropertyAccessResolver(): bool + { + return $this->canAccessProtectedProperty('ProbeClass', 'ProbeClass'); + } +} + +class CompilerPhaseTest extends \PHPUnit\Framework\TestCase +{ + public function testPropertyAccessResolverCannotBeUsedOutsideConvertPhase(): void + { + $compiler = CompilerPhaseProbe::createProbe(ROOT_PATH); + + $this->expectException(TestError::class); + $this->expectExceptionMessage('PropertyAccessResolver can only be used during convert phase'); + + $compiler->probePropertyAccessResolver(); + } + + public function testPropertyAccessResolverCannotBeUsedDuringPreparePhase(): void + { + $compiler = CompilerPhaseProbe::createProbe(ROOT_PATH); + $compiler->enterPreparePhase(); + + $this->expectException(TestError::class); + $this->expectExceptionMessage('PropertyAccessResolver can only be used during convert phase'); + + $compiler->probePropertyAccessResolver(); + } + + public function testPropertyAccessResolverCanBeUsedDuringConvertPhase(): void + { + $compiler = CompilerPhaseProbe::createProbe(ROOT_PATH); + $compiler->enterConvertPhase(); + + $this->assertTrue($compiler->probePropertyAccessResolver()); + } +} diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index e4a6d079..8ef0bf04 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -43,6 +43,7 @@ use PhpAot\Php\Platform\Macos; use PhpAot\Php\Platform\PlatformBase; use PhpAot\Php\Platform\PlatformFactory; use PhpAot\Php\Platform\Windows; +use PhpAot\Php\Resolver\PropertyAccessResolver; use PhpParser\Modifiers; use PhpParser\Node; use PhpParser\Node\ArrayItem; @@ -163,8 +164,12 @@ class CompilerBase extends \PhpAot\Core\Translator public const string BUILD_MODE_EXT = 'ext'; public const string ENTRY_FUNCTION = 'main'; public const string PHPX_VENDOR_DIR = '/vendor/swoole/phpx'; + protected const string PHASE_IDLE = 'idle'; + protected const string PHASE_PREPARE = 'prepare'; + protected const string PHASE_CONVERT = 'convert'; protected string $lang = 'PHP'; + protected string $compilerPhase = self::PHASE_IDLE; protected string $cppCompiler = ''; protected array $literalStrings = []; protected int $literalStringIndex = 0; @@ -845,6 +850,25 @@ class CompilerBase extends \PhpAot\Core\Translator $this->methodDef = null; } + protected function enterCompilerPhase(string $phase): string + { + $previous = $this->compilerPhase; + $this->compilerPhase = $phase; + return $previous; + } + + protected function restoreCompilerPhase(string $phase): void + { + $this->compilerPhase = $phase; + } + + protected function assertCompilerPhase(string $expected, string $feature): void + { + if ($this->compilerPhase !== $expected) { + $this->error("Internal compiler error: {$feature} can only be used during {$expected} phase, current phase is {$this->compilerPhase}"); + } + } + protected function resetClass(): void { $this->class = ''; @@ -5810,31 +5834,29 @@ class CompilerBase extends \PhpAot\Core\Translator return null; } + private function createPropertyAccessResolver(): PropertyAccessResolver + { + $this->assertCompilerPhase(self::PHASE_CONVERT, 'PropertyAccessResolver'); + return new PropertyAccessResolver( + fn(string $class): ?ClassDef => $this->classes[$this->escapeClass($class)] ?? null, + fn(string $class): string => $this->classExtends[strtolower(ltrim($class, '\\'))] ?? '', + fn(NodeAbstract $expr, string $message) => $this->fatalError($expr, $message), + ); + } + protected function isSameClassName(string $classA, string $classB): bool { - return strcasecmp(ltrim($classA, '\\'), ltrim($classB, '\\')) === 0; + return $this->createPropertyAccessResolver()->isSameClassName($classA, $classB); } protected function isSameOrSubclassOf(string $class, string $parent): bool { - $class = strtolower(ltrim($class, '\\')); - $parent = strtolower(ltrim($parent, '\\')); - while ($class !== '') { - if ($class === $parent) { - return true; - } - $class = $this->classExtends[$class] ?? ''; - } - return false; + return $this->createPropertyAccessResolver()->isSameOrSubclassOf($class, $parent); } protected function canAccessProtectedProperty(string $scope, string $declaringClass): bool { - if ($scope === '') { - return false; - } - return $this->isSameOrSubclassOf($scope, $declaringClass) - || $this->isSameOrSubclassOf($declaringClass, $scope); + return $this->createPropertyAccessResolver()->canAccessProtectedProperty($scope, $declaringClass); } /** @@ -5843,49 +5865,12 @@ class CompilerBase extends \PhpAot\Core\Translator */ protected function findNativeProperty(NodeAbstract $expr, string $property, string $class, bool $static = false): ?string { - $class = ltrim($class, '\\'); - $findClass = $class; $scope = $this->class ? $this->getFullClassName() : ''; - $propertyDef = null; - $classDef = null; - while (true) { - if ($this->hasClass($findClass)) { - $classDef = $this->getClass($findClass); - if ($classDef->hasProperty($property)) { - $propertyDef = $classDef->getProperty($property); - if (!$static and $propertyDef->isStatic()) { - $this->fatalError($expr, "Cannot access static property `{$class}::\${$property}` as non-static instance property."); - } - if ($static and !$propertyDef->isStatic()) { - $this->fatalError($expr, "Cannot access non-static property `{$class}::\${$property}` as static property."); - } - if ($propertyDef->isPublic()) { - break; - } - if ($propertyDef->isProtected()) { - if ($this->canAccessProtectedProperty($scope, $findClass)) { - break; - } - $displayClass = ltrim($class, '\\'); - $this->fatalError($expr, "Cannot access protected property `{$property}` of class `{$displayClass}`"); - } else { - if ($this->isSameClassName($scope, $findClass)) { - break; - } - $displayClass = ltrim($class, '\\'); - $this->fatalError($expr, "Cannot access private property `{$property}` of class `{$displayClass}`"); - } - } elseif ($classDef->extends) { - $findClass = $classDef->extends; - continue; - } - } - break; - } - if ($propertyDef and $classDef) { - $expr->setAttribute('nativePropertyDef', $propertyDef); - $expr->setAttribute('nativeClassDef', $classDef); - return $this->getPropertyOffset($classDef->getNamespacedName(false), $property); + $result = $this->createPropertyAccessResolver()->resolveNativeProperty($expr, $property, $class, $scope, $static); + if ($result !== null) { + $expr->setAttribute('nativePropertyDef', $result->propertyDef); + $expr->setAttribute('nativeClassDef', $result->classDef); + return $this->getPropertyOffset($result->classDef->getNamespacedName(false), $result->property); } return null; } diff --git a/src/Php/Preprocessor.php b/src/Php/Preprocessor.php index 05a1988a..9422772a 100644 --- a/src/Php/Preprocessor.php +++ b/src/Php/Preprocessor.php @@ -93,62 +93,67 @@ class Preprocessor extends CompilerBase public function prepareFile(string $file): void { - $phpCode = $this->loadFile($file); - $this->symbolCallInFile[$this->file] = []; - $this->resetFile(); - $this->resetFunction(); - $this->resetMethod(); - $this->resetClass(); - $this->resetNamespace(); - - $this->climate->info('prepare: ' . $this->getRelativePath($this->file)); + $previousPhase = $this->enterCompilerPhase(self::PHASE_PREPARE); try { - $ast = $this->parser->parse($phpCode); - } catch (\PhpParser\Error $e) { - $this->climate->red("Fatal error: {$e->getMessage()} in {$this->file}"); - throw new SyntaxError($e->getMessage(), $e->getCode()); - } - - $traverser = new NodeTraverser(); - $traverser->addVisitor(new Visitor()); - $stmts = $traverser->traverse($ast); - - foreach ($stmts as $v) { - $type = $v->getType(); - switch ($type) { - case 'Stmt_Namespace': - $this->prepareNamespace($v); - break; - case 'Stmt_Enum': - case 'Stmt_Class': - case 'Stmt_Trait': - $this->prepareClass($v); - break; - case 'Stmt_Interface': - $this->parseInterface($v); - break; - case 'Stmt_Function': - $this->prepareFunction($v); - break; - case 'Stmt_Use': - $this->parseUse($v); - break; - case 'Stmt_GroupUse': - $this->parseGroupUse($v); - break; - case 'Stmt_Declare': - case 'Stmt_Nop': - break; - case 'Stmt_Const': - $this->parseConstDef($v); - break; - case 'Stmt_Expression': - $this->foundStrayCode($v); - break; - default: - $this->fatalError($v, 'Unsupported statement: ' . $type); - break; + $phpCode = $this->loadFile($file); + $this->symbolCallInFile[$this->file] = []; + $this->resetFile(); + $this->resetFunction(); + $this->resetMethod(); + $this->resetClass(); + $this->resetNamespace(); + + $this->climate->info('prepare: ' . $this->getRelativePath($this->file)); + try { + $ast = $this->parser->parse($phpCode); + } catch (\PhpParser\Error $e) { + $this->climate->red("Fatal error: {$e->getMessage()} in {$this->file}"); + throw new SyntaxError($e->getMessage(), $e->getCode()); + } + + $traverser = new NodeTraverser(); + $traverser->addVisitor(new Visitor()); + $stmts = $traverser->traverse($ast); + + foreach ($stmts as $v) { + $type = $v->getType(); + switch ($type) { + case 'Stmt_Namespace': + $this->prepareNamespace($v); + break; + case 'Stmt_Enum': + case 'Stmt_Class': + case 'Stmt_Trait': + $this->prepareClass($v); + break; + case 'Stmt_Interface': + $this->parseInterface($v); + break; + case 'Stmt_Function': + $this->prepareFunction($v); + break; + case 'Stmt_Use': + $this->parseUse($v); + break; + case 'Stmt_GroupUse': + $this->parseGroupUse($v); + break; + case 'Stmt_Declare': + case 'Stmt_Nop': + break; + case 'Stmt_Const': + $this->parseConstDef($v); + break; + case 'Stmt_Expression': + $this->foundStrayCode($v); + break; + default: + $this->fatalError($v, 'Unsupported statement: ' . $type); + break; + } } + } finally { + $this->restoreCompilerPhase($previousPhase); } } diff --git a/src/Php/Resolver/PropertyAccessResolver.php b/src/Php/Resolver/PropertyAccessResolver.php new file mode 100644 index 00000000..dd092c27 --- /dev/null +++ b/src/Php/Resolver/PropertyAccessResolver.php @@ -0,0 +1,109 @@ +getParentClass)($class); + } + return false; + } + + public function canAccessProtectedProperty(string $scope, string $declaringClass): bool + { + if ($scope === '') { + return false; + } + return $this->isSameOrSubclassOf($scope, $declaringClass) + || $this->isSameOrSubclassOf($declaringClass, $scope); + } + + public function resolveNativeProperty( + NodeAbstract $expr, + string $property, + string $class, + string $scope, + bool $static = false, + ): ?PropertyAccessResult { + $class = ltrim($class, '\\'); + $findClass = $class; + + while (true) { + $classDef = ($this->getClassDef)($findClass); + if ($classDef === null) { + break; + } + + if ($classDef->hasProperty($property)) { + $propertyDef = $classDef->getProperty($property); + if (!$static && $propertyDef->isStatic()) { + $this->fatal($expr, "Cannot access static property `{$class}::\${$property}` as non-static instance property."); + } + if ($static && !$propertyDef->isStatic()) { + $this->fatal($expr, "Cannot access non-static property `{$class}::\${$property}` as static property."); + } + if ($propertyDef->isPublic()) { + return new PropertyAccessResult($class, $findClass, $property, $classDef, $propertyDef); + } + if ($propertyDef->isProtected()) { + if ($this->canAccessProtectedProperty($scope, $findClass)) { + return new PropertyAccessResult($class, $findClass, $property, $classDef, $propertyDef); + } + $displayClass = ltrim($class, '\\'); + $this->fatal($expr, "Cannot access protected property `{$property}` of class `{$displayClass}`"); + } + if ($this->isSameClassName($scope, $findClass)) { + return new PropertyAccessResult($class, $findClass, $property, $classDef, $propertyDef); + } + $displayClass = ltrim($class, '\\'); + $this->fatal($expr, "Cannot access private property `{$property}` of class `{$displayClass}`"); + } + + if (!$classDef->extends) { + break; + } + $findClass = $classDef->extends; + } + + return null; + } + + private function fatal(NodeAbstract $expr, string $message): never + { + ($this->fatalError)($expr, $message); + } +} diff --git a/src/Php/Resolver/PropertyAccessResult.php b/src/Php/Resolver/PropertyAccessResult.php new file mode 100644 index 00000000..18c0fc8f --- /dev/null +++ b/src/Php/Resolver/PropertyAccessResult.php @@ -0,0 +1,24 @@ +loadFile($file); - $this->localHeaders = []; - while (true) { - try { - $cppCode = $this->doConvert($phpCode); - $cppFile = $this->getCppFile($file); - $this->save($cppCode, $cppFile); - $this->phpSrcFiles[] = $file; - // 生成 stub 文件,依赖 convert 阶段的 use 等信息 - $this->genStubFile($this->file); - return $cppFile; - } catch (Redo $e) { - continue; + $previousPhase = $this->enterCompilerPhase(self::PHASE_CONVERT); + try { + $file = realpath($file); + $phpCode = $this->loadFile($file); + $this->localHeaders = []; + while (true) { + try { + $cppCode = $this->doConvert($phpCode); + $cppFile = $this->getCppFile($file); + $this->save($cppCode, $cppFile); + $this->phpSrcFiles[] = $file; + // 生成 stub 文件,依赖 convert 阶段的 use 等信息 + $this->genStubFile($this->file); + return $cppFile; + } catch (Redo $e) { + continue; + } } + } finally { + $this->restoreCompilerPhase($previousPhase); } } diff --git a/tests/aot/closure/read-private-prop-in-closure.phpt b/tests/aot/closure/read-private-prop-in-closure.phpt new file mode 100644 index 00000000..14727d6f --- /dev/null +++ b/tests/aot/closure/read-private-prop-in-closure.phpt @@ -0,0 +1,35 @@ +--TEST-- +closure 001 +--FILE-- +arr; + }); + } +} + +function main() +{ + $foo = new Foo(); + $foo->run(); +} +?> +--EXPECT-- +array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) +} diff --git a/tests/aot/functions/return-value.phpt b/tests/aot/functions/return-value.phpt index 651046de..00ff4a68 100644 --- a/tests/aot/functions/return-value.phpt +++ b/tests/aot/functions/return-value.phpt @@ -2,9 +2,15 @@ object link operator --FILE--