Compare commits

...

14 Commits

Author SHA1 Message Date
韩天峰 74274f8328 fix(compiler): defer cache IDs to convert phase 6 hours ago
韩天峰 6693d95cd6 ci: restore full PHP test matrix 7 hours ago
韩天峰 8f32497de7 refactor(compiler): strongly type symbol cache IDs 7 hours ago
韩天峰 1b235d0769 test(compiler): reject mixed cache lifetime IDs 7 hours ago
韩天峰 8c6ea1d14e fix(build): make source discovery deterministic 7 hours ago
韩天峰 0ac6dfe499 fix(compiler): keep method cache lifetime aligned with class 7 hours ago
韩天峰 a6cc11c6d7 ci: narrow class cache debugging workflow 7 hours ago
韩天峰 2d14c9e432 ``` 7 hours ago
韩天峰 7f587749f1 chore(ci): replace custom PHP setup action with community action 8 hours ago
韩天峰 821f639e8f fix(ci): remove compiler flag from PHPT test runner 8 hours ago
韩天峰 983333dc2e refactor(php): conditionally install and enable OPcache based on availability 8 hours ago
韩天峰 978e553480 chore(ci): migrate PHP setup to custom action with Ubuntu 24.04 8 hours ago
韩天峰 e4817819dc fix(ci): remove embed extension from PHP setup and add explicit embed library linking 9 hours ago
韩天峰 8a30c5e58d rm composer.lock 10 hours ago
  1. 90
      .github/copilot-instructions.md
  2. 34
      .github/workflows/tests.yml
  3. 1
      .gitignore
  4. 115
      phpunit/src/CompilerBaseApiTest.php
  5. 9
      phpunit/src/FileScannerTest.php
  6. 11
      phpunit/src/PreprocessorTest.php
  7. 45
      run-tests.php
  8. 6
      src/Build/FileScanner.php
  9. 86
      src/Build/SourcePipelineTrait.php
  10. 2
      src/Build/WasmInterfaceGenerator.php
  11. 55
      src/CompilerBase.php
  12. 7
      src/Entity/ArgInfo.php
  13. 2
      src/Entity/ConstantDef.php
  14. 2
      src/Entity/PropertyDef.php
  15. 2
      src/Generator/CallArgumentGenerator.php
  16. 2
      src/Generator/DefaultArgumentGenerator.php
  17. 286
      src/Preprocessor.php
  18. 9
      src/Resolver/DeclarationSymbolTrait.php
  19. 84
      src/Translator.php

@ -1,90 +0,0 @@
# Copilot instructions for this repository
## Project overview
TypePHP is a PHP native compilation project. Its `tpc` command is TypePHP Compiler (AOT), which translates PHP source into C++, then compiles and links it into a native binary or a PHP extension. The primary entrypoint boots `src/compiler.php`; that drives `TypePhp\Translator` through a fixed pipeline:
1. `prepare()` scans files, parses ASTs, collects symbols, and topologically sorts PHP files by cross-file symbol usage.
2. `convert()` turns PHP ASTs into generated `.cc` files while passing through native source files (`.cpp`, `.c`, `.s`, `.m`, `.mm`).
3. `compile()` chooses the platform/compiler backend, generates support sources and headers, and compiles sources, using `pcntl` parallelism when available.
4. `build()` links object files into the final executable or extension.
`src/CompilerBase.php` contains most PHP-to-C++ translation logic and mixes in many traits for syntax handling and optimizations. `src/Preprocessor.php` owns dependency discovery and file ordering. Platform-specific behavior lives under `src/Platform/`, compiler backends under `src/Backend/`, and metadata/state objects under `src/Entity/` and `src/Context/`.
## Setup and build commands
The repo expects PHP 8.4+, GCC 9+ with C++17, CMake 3.24+, and a compiled `swoole/phpx` dependency. Install PHP dependencies with:
```bash
composer install
```
Build `phpx` before relying on compiler runs:
```bash
cd vendor/swoole/phpx
cmake .
make -j32
```
Compile a project, directory, single file, or `project.yml`:
```bash
./tpc <path-to-project-or-file>
./tpc <path> -O2
./tpc <path> --mode=ext -o <output_name>
```
## Test commands
Run the PHPUnit suite:
```bash
./vendor/bin/phpunit
```
Run a single PHPUnit file or a single test method:
```bash
./vendor/bin/phpunit phpunit/src/Platform/PlatformTest.php
./vendor/bin/phpunit --filter testWindowsBasic phpunit/src/Platform/PlatformTest.php
```
Run PHPT integration tests:
```bash
php run-tests.php tests/compiler/
php run-tests.php tests/compiler/arrays.phpt
```
For parser/runtime comparison without AOT compilation, there are docs using:
```bash
php run-tests.php --no-aot tests/compiler/arrow-functions.phpt
```
## Formatting
The repo ships a PHP CS Fixer config in `.php-cs-fixer.dist.php`:
```bash
php vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php <path>
```
Generated C++ is auto-formatted by the compiler itself when `clang-format` is available.
## Configuration and repository conventions
- `project.yml` is the project-level build config. Important keys include `name`, `build-mode`, `cxx-std`, `cxx-flags`, `ld-flags`, `sources`, `ignore`, and `resource`.
- Command-line options intentionally override YAML values. `Translator` parses YAML first, then applies CLI arguments last.
- YAML parsing accepts both hyphenated and underscored variants for several keys, but existing examples use hyphenated names such as `build-mode` and `cxx-std`.
- In `bin` mode, compiled programs must define `main()`. In `ext` mode they do not.
- File discovery is mixed-language by design: PHP is translated, while native sources are compiled directly if they appear in configured sources.
- Generated files are written under `build/`, with generated C++ paths mirroring the source tree and generated headers under `build/include/`.
- Platform/compiler selection is centralized: `PlatformFactory` detects the OS, and `CompilerFactory` picks the backend (`Gcc`, `Clang`, `Msvc`) with environment/config overrides.
## Test-specific conventions
- PHPUnit tests for compiler internals should use `CompilerTest::create(ROOT_PATH)`, which enables test mode instead of normal fatal exits.
- `phpunit/bootstrap.php` exposes a `BaseTest::exec()` helper that expects compilation failures to surface as `TypePhp\Exception\TestError`.
- PHPT end-to-end tests live in `tests/compiler/`; existing guidance and examples generally put executable test logic inside a `main()` function.

@ -43,7 +43,6 @@ jobs:
with:
php-version: ${{ matrix.php }}
coverage: none
extensions: embed
ini-values: precision=17, memory_limit=4G, error_reporting=E_ERROR|E_WARNING, display_errors=1, display_startup_errors=1, log_errors=0
tools: composer:v2
@ -114,7 +113,7 @@ jobs:
with:
php-version: ${{ matrix.php }}
coverage: none
extensions: embed, curl, redis, mbstring, ffi
extensions: curl, redis, mbstring, ffi
ini-values: ffi.enable=1, phpy.enable_operator_overloading=0, precision=17, memory_limit=4G, error_reporting=E_ERROR|E_WARNING, display_errors=1, display_startup_errors=1, log_errors=0
tools: composer:v2
@ -170,6 +169,7 @@ jobs:
PHPX_HOME: ${{ github.workspace }}/vendor/swoole/phpx
NO_INTERACTION: 1
REPORT_EXIT_STATUS: 1
TYPEPHP_PHPT_GENERATED_ARTIFACT_DIR: ${{ github.workspace }}/build/phpt-generated
steps:
- name: Checkout TypePHP
@ -192,6 +192,30 @@ jobs:
sudo apt-get update
sudo apt-get install --yes build-essential cmake libgmp-dev libmpfr-dev pkg-config python3-dev
- name: Configure version-matched PHP embed library
shell: bash
run: |
embed_package="libphp${{ matrix.php }}-embed"
php_version="$(php-config --version)"
embed_version="$(dpkg-query --show --showformat='${Version}' "${embed_package}")"
case "${embed_version}" in
"${php_version}"*) ;;
*) echo "PHP embed package ${embed_version} does not match PHP ${php_version}" >&2; exit 1 ;;
esac
embed_library="$(dpkg-query --listfiles "${embed_package}" \
| sed -n '/\/libphp[0-9][^/]*\.so$/ { p; q; }')"
test -n "${embed_library}"
test -f "${embed_library}"
php_home="${RUNNER_TEMP}/typephp-php-${{ matrix.php }}"
mkdir -p "${php_home}/bin" "${php_home}/include" "${php_home}/lib"
ln -s "$(command -v php-config)" "${php_home}/bin/php-config"
ln -s "$(php-config --include-dir)" "${php_home}/include/php"
ln -s "${embed_library}" "${php_home}/lib/libphp.so"
echo "PHP_HOME=${php_home}" >> "${GITHUB_ENV}"
echo "Using ${embed_library} from ${embed_package} ${embed_version} for PHP ${php_version}"
- name: Install Composer dependencies
run: composer install --prefer-dist --no-progress
@ -220,7 +244,8 @@ jobs:
shell: bash
run: |
test -f "${PHPX_HOME}/lib/libphpx.so"
echo "LD_LIBRARY_PATH=${PHPX_HOME}/lib:$(php-config --prefix)/lib" >> "${GITHUB_ENV}"
test -f "${PHP_HOME}/lib/libphp.so"
echo "LD_LIBRARY_PATH=${PHPX_HOME}/lib:${PHP_HOME}/lib" >> "${GITHUB_ENV}"
- name: Build bootstrap TypePHP compiler
run: |
@ -232,6 +257,7 @@ jobs:
run: |
php -v
php --ini
ldd ./tpc | grep -E 'libphp(x)?[0-9.]*\.so'
cmake --version
c++ --version
@ -251,6 +277,8 @@ jobs:
path: |
build/failed-tests.txt
build/test-results.txt
build/**/*.cc
build/**/*.h
php_test_results_*.txt
tests/compiler/**/*.diff
tests/compiler/**/*.log

1
.gitignore vendored

@ -11,6 +11,7 @@
/.php-cs-fixer.cache
/.phpunit.cache
/__pycache__
/composer.lock
*.o
*.dll
*.so

@ -78,6 +78,114 @@ class CompilerBaseApiTest extends TestCase
return $m->invoke($this->compiler, ...$args);
}
public function testMethodCacheKeepsPreviouslyAssignedClassLifetime(): void
{
$this->setPropertyValue('compilerPhase', 'convert');
// The two maps have independent ID spaces. This reproduces the tpc
// bootstrap ordering where both classes occupied slot zero.
$this->setPropertyValue('classMap', ['LateKnownClass' => 0]);
$this->setPropertyValue('classIndex', 1);
$this->setPropertyValue('persistentClassMap', ['StableResolver' => 0]);
$this->setPropertyValue('persistentClassIndex', 1);
$methodPtr = $this->invokeMethod('getMethodPtr', 'LateKnownClass', 'run');
$this->assertStringStartsWith('get_method(RequestFuncId{0}, ', $methodPtr);
$this->assertStringContainsString(', RequestClassId{0}, ', $methodPtr);
$this->assertSame(
['LateKnownClass::run' => 0],
$this->getPropertyValue('funcMap'),
);
$this->assertSame([], $this->getPropertyValue('persistentFuncMap'));
}
public function testMethodCacheRejectsMismatchedLifetimeDomains(): void
{
$this->setPropertyValue('compilerPhase', 'convert');
$this->setPropertyValue('classMap', ['LateKnownClass' => 0]);
$this->setPropertyValue('classIndex', 1);
$this->setPropertyValue('persistentFuncMap', ['LateKnownClass::run' => 0]);
$this->setPropertyValue('persistentFuncIndex', 1);
$this->expectException(\LogicException::class);
$this->expectExceptionMessage(
'Cache lifetime mismatch for LateKnownClass::run: method ID 0 is persistent, class ID 0 is request-local',
);
$this->invokeMethod('getMethodPtr', 'LateKnownClass', 'run');
}
public function testPrepareStoresDeclarationAstsWithoutAllocatingCacheIds(): void
{
$consumer = $this->testDir . '/consumer.php';
$declaration = $this->testDir . '/late.php';
file_put_contents($consumer, <<<'PHP'
<?php
namespace CachePhase;
final class Defaults
{
public const string VALUE = LateClass::VALUE;
public string $property = LateClass::VALUE;
public function read(string $value = LateClass::VALUE): string
{
return $value;
}
}
PHP);
file_put_contents($declaration, <<<'PHP'
<?php
namespace CachePhase;
final class LateClass
{
public const string VALUE = 'ready';
}
PHP);
// Deliberately prepare the consumer first. This was the bootstrap
// ordering that used to assign a request ID before LateClass became
// visible, then assign persistent IDs to later method lookups.
$this->compiler->prepareFile($consumer);
$this->compiler->prepareFile($declaration);
foreach (['classMap', 'persistentClassMap', 'funcMap', 'persistentFuncMap', 'persistentPropMap'] as $map) {
$this->assertSame([], $this->getPropertyValue($map), $map);
}
$defaults = $this->compiler->getClassDef('CachePhase\\Defaults');
$this->assertNotNull($defaults);
$this->assertInstanceOf(
\PhpParser\Node\Expr\ClassConstFetch::class,
$defaults->getConstant('VALUE')->valueExpr,
);
$this->assertSame('', $defaults->getConstant('VALUE')->value);
$this->assertNotNull($defaults->getProperty('property')->defaultExpr);
$this->assertTrue($defaults->getMethod('read')->functionDef->argInfoList[0]->hasDefaultValue());
$this->assertSame('', $defaults->getMethod('read')->functionDef->argInfoList[0]->default);
$this->setPropertyValue('compilerPhase', 'convert');
$this->compiler->finalizeDeclarationExpressions([$consumer, $declaration]);
$this->assertSame([], $this->getPropertyValue('classMap'));
$this->assertArrayHasKey(
'CachePhase\\LateClass',
$this->getPropertyValue('persistentClassMap'),
);
$this->assertNotSame('', $defaults->getConstant('VALUE')->value);
$this->assertNotNull($defaults->getProperty('property')->default);
$this->assertNotSame('', $defaults->getMethod('read')->functionDef->argInfoList[0]->default);
}
public function testCacheIdAllocationIsRejectedOutsideConvertPhase(): void
{
$this->setPropertyValue('compilerPhase', 'prepare');
$this->expectException(TestError::class);
$this->expectExceptionMessage(
'class cache ID allocation can only be used during convert phase, current phase is prepare',
);
$this->invokeMethod('getClassId', 'ForbiddenDuringPrepare');
}
private function fixturePath(string $file): string
{
return __DIR__ . '/../code/compiler_api/' . $file;
@ -1224,8 +1332,13 @@ YAML);
'static php::PersistentCacheSlot<uint32_t> php_persistent_property_map',
$extension,
);
$this->assertStringContainsString('enum class RequestClassId : uint32_t {};', $data);
$this->assertStringContainsString('enum class PersistentClassId : uint32_t {};', $data);
$this->assertStringContainsString('enum class RequestFuncId : uint32_t {};', $data);
$this->assertStringContainsString('enum class PersistentFuncId : uint32_t {};', $data);
$this->assertStringContainsString('enum class PersistentPropertyId : uint32_t {};', $data);
$this->assertStringContainsString('get_persistent_class', $extension);
$this->assertStringContainsString('php::getPersistentCache(php_persistent_class_map[class_id]', $extension);
$this->assertStringContainsString('php::getPersistentCache(php_persistent_class_map[index]', $extension);
$this->assertStringContainsString('for (auto &slot : php_persistent_class_map)', $extension);
$this->assertStringContainsString('php::resetPersistentCache(slot);', $extension);
$this->assertStringNotContainsString('#ifdef ZTS', $data);

@ -141,6 +141,15 @@ class FileScannerTest extends TestCase
$scanner = new FileScanner($tmpDir);
$files = $scanner->scan();
$this->assertSame([
$tmpDir . '/helper.c',
$tmpDir . '/lib.m',
$tmpDir . '/main.php',
$tmpDir . '/math.S',
$tmpDir . '/module.cc',
$tmpDir . '/sub/nested.cpp',
], $files);
// Python and Markdown should not be included
$this->assertContains($tmpDir . '/main.php', $files);
$this->assertContains($tmpDir . '/helper.c', $files);

@ -287,7 +287,7 @@ class PreprocessorTest extends TestCase
$this->assertNotEmpty($functionDef->returnTypeCheck);
}
public function testPrepareFileParsesInterfaceArrayConstantInitExpr(): void
public function testInterfaceArrayConstantIsLoweredOnlyDuringConvert(): void
{
$file = __DIR__ . '/../code/interface_array_constant.php';
@ -298,6 +298,15 @@ class PreprocessorTest extends TestCase
$constant = $interfaces['interfacearrayconstant']->constants['ITEMS'];
$this->assertSame('php::Array', $constant->type);
$this->assertInstanceOf(\PhpParser\Node\Expr\Array_::class, $constant->valueExpr);
$this->assertSame('', $constant->value);
$this->assertSame([], $this->getProperty('classMap'));
$this->assertSame([], $this->getProperty('persistentClassMap'));
$this->assertSame([], $this->getProperty('funcMap'));
$this->assertSame([], $this->getProperty('persistentFuncMap'));
$this->setProperty('compilerPhase', 'convert');
$this->compiler->finalizeDeclarationExpressions([$file]);
$this->assertStringContainsString('php::Array', $constant->value);
}

@ -1686,6 +1686,9 @@ escape:
error("$testsInProgress test batches “in progress”, which is less than zero. THIS SHOULD NOT HAPPEN.");
}
if ($PHP_FAILED_TESTS['FAILED']) {
copy_aot_generated_sources_for_artifact($aot_parallel_root);
}
remove_directory($aot_parallel_root);
$aot_parallel_root = null;
}
@ -4624,6 +4627,48 @@ function ensure_directory_exists(string $directory): void
}
}
/**
* Preserve generated C++ and header files before the parallel PHPT workspace
* is removed. CI sets TYPEPHP_PHPT_GENERATED_ARTIFACT_DIR to a directory under
* the checkout so actions/upload-artifact can include these diagnostics.
*/
function copy_aot_generated_sources_for_artifact(string $sourceRoot): void
{
$artifactRoot = getenv('TYPEPHP_PHPT_GENERATED_ARTIFACT_DIR');
if (!is_string($artifactRoot) || $artifactRoot === '' || !is_dir($sourceRoot)) {
return;
}
$sourceRoot = rtrim($sourceRoot, '/\\');
$destinationRoot = rtrim($artifactRoot, '/\\')
. DIRECTORY_SEPARATOR . basename($sourceRoot);
try {
$copied = 0;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($sourceRoot, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY,
);
foreach ($iterator as $entry) {
if (!$entry->isFile() || !in_array(strtolower($entry->getExtension()), ['cc', 'h'], true)) {
continue;
}
$relativePath = substr($entry->getPathname(), strlen($sourceRoot) + 1);
$destination = $destinationRoot . DIRECTORY_SEPARATOR . $relativePath;
ensure_directory_exists(dirname($destination));
if (!copy($entry->getPathname(), $destination)) {
throw new RuntimeException('Cannot copy generated source: ' . $entry->getPathname());
}
$copied++;
}
fwrite(STDERR, "Preserved {$copied} generated PHPT source files in {$destinationRoot}" . PHP_EOL);
} catch (Throwable $e) {
fwrite(STDERR, 'Warning: failed to preserve PHPT generated sources: '
. $e->getMessage() . PHP_EOL);
}
}
function remove_directory(string $directory): void
{
if (!is_dir($directory)) {

@ -100,6 +100,12 @@ class FileScanner
}
}
// RecursiveDirectoryIterator follows filesystem directory-entry order,
// which differs between a fresh checkout and a long-lived worktree.
// Preparation allocates symbol-cache IDs while visiting this list, so
// keep both generated code and cache classification deterministic.
sort($files, SORT_STRING);
return $files;
}

@ -249,49 +249,59 @@ trait SourcePipelineTrait
public function convert(array $files): array
{
$sourceFiles = [];
$validSourceCount = 0;
// 生成 C++ 文件
foreach ($files as $k => $file) {
try {
if (FileScanner::isPhpFile($file)) {
$cppFile = $this->convertFile($file);
} elseif (FileScanner::isNativeSourceFile($file)) {
$cppFile = $file;
} else {
continue;
}
$validSourceCount++;
if ($cppFile !== null) {
$sourceFiles[] = $cppFile;
$previousPhase = $this->enterCompilerPhase(self::PHASE_CONVERT);
try {
// All declarations are now known. Lower declaration constant
// expressions before translating any function body so cache IDs
// are assigned exclusively in the convert phase.
$this->finalizeDeclarationExpressions($files);
$sourceFiles = [];
$validSourceCount = 0;
// 生成 C++ 文件
foreach ($files as $k => $file) {
try {
if (FileScanner::isPhpFile($file)) {
$cppFile = $this->convertFile($file);
} elseif (FileScanner::isNativeSourceFile($file)) {
$cppFile = $file;
} else {
continue;
}
$validSourceCount++;
if ($cppFile !== null) {
$sourceFiles[] = $cppFile;
}
} catch (Unsupported $e) {
echo ' unsupported syntax: ' . $e->getMessage() . "\n";
echo ' skip: ' . $file . "\n";
unset($files[$k]);
}
} catch (Unsupported $e) {
echo ' unsupported syntax: ' . $e->getMessage() . "\n";
echo ' skip: ' . $file . "\n";
unset($files[$k]);
}
}
// A valid PHP input may intentionally emit no standalone translation
// unit (for example a compile-time trait or an interface). The shared
// extension source still carries its runtime metadata, so only reject
// an input set in which no supported source was converted at all.
if ($validSourceCount === 0) {
$this->stop('No valid source file found');
}
// A valid PHP input may intentionally emit no standalone translation
// unit (for example a compile-time trait or an interface). The shared
// extension source still carries its runtime metadata, so only reject
// an input set in which no supported source was converted at all.
if ($validSourceCount === 0) {
$this->stop('No valid source file found');
}
// A WASI library publishes WIT/Component exports rather than a native
// TypePHP shared-library ABI, so a PHP import stub would be misleading.
if ($this->isBuildModeLib() && !$this->isWasiTarget()) {
$this->genLibraryImportStub($files);
}
// A WASI library publishes WIT/Component exports rather than a native
// TypePHP shared-library ABI, so a PHP import stub would be misleading.
if ($this->isBuildModeLib() && !$this->isWasiTarget()) {
$this->genLibraryImportStub($files);
}
// 生成构建期内部头文件:函数声明、运行时数据声明
$this->genFunctionDeclarations($this->getIncludeDir() . "/php_{$this->targetName}_func_decl.h");
$this->genDataDeclarations($this->getIncludeDir() . "/php_{$this->targetName}_data_decl.h");
// 生成扩展模块源文件
$sourceFiles[] = $this->genExtension();
// 生成构建期内部头文件:函数声明、运行时数据声明
$this->genFunctionDeclarations($this->getIncludeDir() . "/php_{$this->targetName}_func_decl.h");
$this->genDataDeclarations($this->getIncludeDir() . "/php_{$this->targetName}_data_decl.h");
// 生成扩展模块源文件
$sourceFiles[] = $this->genExtension();
return $sourceFiles;
return $sourceFiles;
} finally {
$this->restoreCompilerPhase($previousPhase);
}
}
}

@ -55,7 +55,7 @@ final class WasmInterfaceGenerator
$parameters = [];
foreach ($function->argInfoList as $argument) {
if ($argument->byRef || $argument->variadic || $argument->default !== '') {
if ($argument->byRef || $argument->variadic || $argument->hasDefaultValue()) {
throw new RuntimeException(
"WasmExport parameter \${$argument->phpName} of {$displayName}() cannot be by-reference, variadic, or optional"
);

@ -316,6 +316,9 @@ class CompilerBase implements PropertyAccessContext
*/
protected array $persistentPropMap = [];
protected int $persistentPropIndex = 0;
/** @var array<string, array<Node\Stmt>> Prepared declaration ASTs keyed by real path. */
protected array $preparedFileAsts = [];
protected bool $declarationExpressionsFinalized = false;
protected const array PHP_RUNTIME_TYPE_MAP = [
'integer' => Type::INT,
'double' => Type::FLOAT,
@ -1288,6 +1291,7 @@ class CompilerBase implements PropertyAccessContext
protected function getClassId(string $className): int
{
$this->assertCompilerPhase(self::PHASE_CONVERT, 'class cache ID allocation');
if (isset($this->classMap[$className])) {
return $this->classMap[$className];
}
@ -1306,6 +1310,7 @@ class CompilerBase implements PropertyAccessContext
protected function getFuncId(string $funcName): int
{
$this->assertCompilerPhase(self::PHASE_CONVERT, 'function cache ID allocation');
if (isset($this->funcMap[$funcName])) {
return $this->funcMap[$funcName];
}
@ -1333,6 +1338,7 @@ class CompilerBase implements PropertyAccessContext
*/
protected function getPropertyId(string $className, string $propName): int
{
$this->assertCompilerPhase(self::PHASE_CONVERT, 'property cache ID allocation');
$key = $className . '::' . $propName;
if (isset($this->persistentPropMap[$key])) {
return $this->persistentPropMap[$key];
@ -1345,8 +1351,10 @@ class CompilerBase implements PropertyAccessContext
protected function getClassEntryPtr(string $className): string
{
$id = $this->getClassId($className);
$helper = isset($this->persistentClassMap[$className]) ? 'get_persistent_class' : 'get_class';
return $helper . '(' . $id . ', ' . $this->getLiteralString($className) . ')';
$persistent = isset($this->persistentClassMap[$className]);
$helper = $persistent ? 'get_persistent_class' : 'get_class';
$idType = $persistent ? 'PersistentClassId' : 'RequestClassId';
return $helper . '(' . $idType . '{' . $id . '}, ' . $this->getLiteralString($className) . ')';
}
/**
@ -1407,24 +1415,41 @@ class CompilerBase implements PropertyAccessContext
throw new \LogicException('Class methods must be resolved through getMethodPtr()');
}
$id = $this->getFuncId($funcName);
$helper = isset($this->persistentFuncMap[$funcName]) ? 'get_persistent_func' : 'get_func';
return $helper . '(' . $id . ', ' . $this->getLiteralString($funcName) . ')';
$persistent = isset($this->persistentFuncMap[$funcName]);
$helper = $persistent ? 'get_persistent_func' : 'get_func';
$idType = $persistent ? 'PersistentFuncId' : 'RequestFuncId';
return $helper . '(' . $idType . '{' . $id . '}, ' . $this->getLiteralString($funcName) . ')';
}
protected function getMethodPtr(string $class, string $method): string
{
$funcId = $this->getFuncId($class . '::' . $method);
$key = $class . '::' . $method;
$funcId = $this->getFuncId($key);
$classId = $this->getClassId($class);
// 方法的稳定性与所属类一致,因此 class_id 必定落在同一张表中
$helper = isset($this->persistentFuncMap[$class . '::' . $method]) ? 'get_persistent_method' : 'get_method';
return $helper . '(' . $funcId . ', ' . $this->getLiteralString($method) . ', ' . $classId . ', ' . $this->getLiteralString($class) . ')';
$persistentMethod = isset($this->persistentFuncMap[$key]);
$persistentClass = isset($this->persistentClassMap[$class]);
if ($persistentMethod !== $persistentClass) {
throw new \LogicException(sprintf(
'Cache lifetime mismatch for %s: method ID %d is %s, class ID %d is %s',
$key,
$funcId,
$persistentMethod ? 'persistent' : 'request-local',
$classId,
$persistentClass ? 'persistent' : 'request-local',
));
}
$helper = $persistentMethod ? 'get_persistent_method' : 'get_method';
$funcIdType = $persistentMethod ? 'PersistentFuncId' : 'RequestFuncId';
$classIdType = $persistentClass ? 'PersistentClassId' : 'RequestClassId';
return $helper . '(' . $funcIdType . '{' . $funcId . '}, ' . $this->getLiteralString($method)
. ', ' . $classIdType . '{' . $classId . '}, ' . $this->getLiteralString($class) . ')';
}
protected function getPropertyOffset(string $class, string $prop): string
{
$propId = $this->getPropertyId($class, $prop);
$classId = $this->getClassId($class);
return 'get_persistent_prop(' . $propId . ', ' . $this->getLiteralString($prop) . ', ' . $classId . ', ' . $this->getLiteralString($class) . ')';
return 'get_persistent_prop(PersistentPropertyId{' . $propId . '}, '
. $this->getLiteralString($prop) . ', ' . $this->getLiteralString($class) . ')';
}
protected function writeLog($msg): void
@ -2837,6 +2862,9 @@ class CompilerBase implements PropertyAccessContext
if ($interfaceConstDef->type === Type::ARRAY) {
return self::PREFIX . $this->getNativeName($interfaceConstDef->name, $interfaceDef->namespace, $interfaceDef->name);
}
if (!$interfaceConstDef->codegenFinalized) {
return false;
}
$expr->setAttribute('nativeConst', $interfaceConstDef);
return $interfaceConstDef->value;
}
@ -2855,6 +2883,13 @@ class CompilerBase implements PropertyAccessContext
if ($constDef->type === Type::ARRAY) {
return self::PREFIX . $this->getNativeName($constDef->name, $classDef->namespace, $classDef->name);
} else {
// Forward constant references may be encountered while the
// declaration-expression pass is still visiting another file.
// Fall back to the Zend class-constant lookup instead of emitting
// an incomplete value; the lookup is cached in the convert phase.
if (!$constDef->codegenFinalized) {
return false;
}
$expr->setAttribute('nativeConst', $constDef);
return $constDef->value;
}

@ -19,6 +19,8 @@ class ArgInfo
public string $type;
public string $default = '';
public ?ArrayInitPlan $arrayInitPlan = null;
/** Original declaration AST; lowered to $default only in the convert phase. */
public ?Expr $defaultExpr = null;
public ?Expr $defaultValue = null;
public string $class = '';
@ -56,4 +58,9 @@ class ArgInfo
/** Original union/nullable AST node. Only set when typeCheck is non-null. */
public ?NodeAbstract $typeNode = null;
public function hasDefaultValue(): bool
{
return $this->defaultExpr !== null;
}
}

@ -19,6 +19,8 @@ class ConstantDef
public string $arrayExpr = '';
public string $class = '';
public ?NodeAbstract $valueExpr = null;
/** True after the declaration AST has been lowered to C++ in convert. */
public bool $codegenFinalized = false;
/** Explicit declared type (e.g. `const int FOO`); null for inferred/untyped constants. */
public ?string $declaredType = null;

@ -19,6 +19,8 @@ class PropertyDef
public int $flags;
public ?string $default = null;
public ?ArrayInitPlan $arrayInitPlan = null;
/** Original declaration AST; lowered to $default only in the convert phase. */
public ?NodeAbstract $defaultExpr = null;
public ?ArrayDefinition $arrayDef = null;
public bool $nullable = false;
/** The declared type is TypePHP's unconstrained, reference-capable `any` type. */

@ -99,7 +99,7 @@ trait CallArgumentGenerator
'Named calls to Native virtual methods cannot skip an earlier optional parameter',
);
}
if ($argInfo->default === '') {
if (!$argInfo->hasDefaultValue()) {
$errorNode = null;
foreach ($callArgs as $a) {
if ($a instanceof Node\Arg && $a->name) {

@ -127,6 +127,6 @@ trait DefaultArgumentGenerator
return true;
}
return $argInfo->default !== '';
return $argInfo->hasDefaultValue();
}
}

@ -303,6 +303,11 @@ class Preprocessor extends CompilerBase
$traverser->addVisitor(new ConstantExpressionValidationVisitor($this->phpVersion));
$traverser->addVisitor(new RuntimeAttributeFactoryLowering($this->file));
$stmts = $traverser->traverse($ast);
// Keep the resolved declaration AST until convert. Defaults and
// constants are validated here, but their C++ expressions are not
// generated until the complete symbol table is available.
$this->preparedFileAsts[$this->file] = $stmts;
$this->declarationExpressionsFinalized = false;
// CompilerTest and embedding users may invoke prepareFile()
// directly instead of the project pipeline. Preserve same-file
// forward Native references for that public entry path as well.
@ -350,6 +355,218 @@ class Preprocessor extends CompilerBase
}
}
/**
* Lower declaration-only constant expressions after every symbol is known.
*
* @param list<string> $files
*/
public function finalizeDeclarationExpressions(array $files): void
{
$this->assertCompilerPhase(self::PHASE_CONVERT, 'declaration expression finalization');
if ($this->declarationExpressionsFinalized) {
return;
}
foreach ($files as $file) {
$path = realpath($file);
if ($path === false || !isset($this->preparedFileAsts[$path])) {
continue;
}
$this->loadFile($path);
$this->resetFile();
$this->resetFunction();
$this->resetMethod();
$this->resetClass();
$this->resetNamespace();
$this->finalizeDeclarationStatementList($this->preparedFileAsts[$path]);
}
$this->declarationExpressionsFinalized = true;
}
/** @param array<Node\Stmt> $statements */
private function finalizeDeclarationStatementList(array $statements): void
{
foreach ($statements as $statement) {
if ($statement instanceof Node\Stmt\Namespace_) {
$this->resetClass();
$this->resetMethod();
$this->resetFunction();
$this->resetNamespace();
$this->namespace = $statement->name ? $this->parseIdentifier($statement->name) : '';
$this->finalizeDeclarationStatementList($statement->stmts);
continue;
}
if ($statement instanceof Node\Stmt\Use_) {
$this->parseUse($statement);
continue;
}
if ($statement instanceof Node\Stmt\GroupUse) {
$this->parseGroupUse($statement);
continue;
}
if ($statement instanceof Node\Stmt\Class_
|| $statement instanceof Node\Stmt\Trait_
|| $statement instanceof Node\Stmt\Enum_
) {
$this->finalizeClassDeclarationExpressions($statement);
continue;
}
if ($statement instanceof Node\Stmt\Interface_) {
$this->finalizeInterfaceDeclarationExpressions($statement);
continue;
}
if ($statement instanceof Node\Stmt\Function_) {
$this->resetClass();
$this->resetMethod();
$this->finalizePreparedFunctionDefaults(
$statement,
$this->getFunction($this->getFunctionName($statement)),
);
continue;
}
if ($statement instanceof Node\Stmt\Const_) {
$this->finalizeGlobalConstantExpressions($statement);
}
}
}
private function finalizeClassDeclarationExpressions(
Node\Stmt\Class_|Node\Stmt\Trait_|Node\Stmt\Enum_ $class,
): void {
$this->resetClass();
$this->class = $this->parseIdentifier($class->name);
$this->classDef = $this->getClass($this->getFullClassName());
foreach ($class->stmts as $statement) {
if ($statement instanceof Node\Stmt\ClassConst) {
foreach ($statement->consts as $constant) {
$name = $this->parseIdentifier($constant->name);
$this->finalizePreparedConstant(
$this->classDef->getConstant($name),
$constant->value,
);
}
continue;
}
if ($statement instanceof Node\Stmt\Property) {
foreach ($statement->props as $property) {
$name = $this->parseIdentifier($property->name);
if ($property->default !== null && $this->classDef->hasProperty($name)) {
$this->finalizePreparedProperty(
$this->classDef->getProperty($name),
$property->default,
);
}
}
continue;
}
if (!$statement instanceof Node\Stmt\ClassMethod) {
continue;
}
$name = $this->getMethodName($statement);
$this->resetMethod();
$this->method = $name;
$this->methodDef = $this->classDef->hasMethod($name)
? $this->classDef->getMethod($name)
: ($this->classDef->hasAbstractMethod($name)
? $this->classDef->getAbstractMethod($name)
: null);
if ($this->methodDef !== null && $this->methodDef->functionDef !== null) {
$this->finalizePreparedFunctionDefaults($statement, $this->methodDef->functionDef);
}
}
}
private function finalizeInterfaceDeclarationExpressions(Node\Stmt\Interface_ $interface): void
{
$this->resetClass();
$this->interface = $this->parseIdentifier($interface->name);
$this->interfaceDef = $this->getInterface($this->getFullClassLikeName());
foreach ($interface->stmts as $statement) {
if ($statement instanceof Node\Stmt\ClassConst) {
foreach ($statement->consts as $constant) {
$name = $this->parseIdentifier($constant->name);
$this->finalizePreparedConstant(
$this->interfaceDef->constants[$name],
$constant->value,
);
}
continue;
}
if (!$statement instanceof Node\Stmt\ClassMethod) {
continue;
}
$name = $this->getMethodName($statement);
$this->resetMethod();
$this->method = $name;
$this->methodDef = $this->interfaceDef->methods[strtolower($name)] ?? null;
if ($this->methodDef !== null && $this->methodDef->functionDef !== null) {
$this->finalizePreparedFunctionDefaults($statement, $this->methodDef->functionDef);
}
}
$this->interface = '';
$this->interfaceDef = null;
}
private function finalizePreparedFunctionDefaults(
Node\Stmt\Function_|Node\Stmt\ClassMethod $function,
FunctionDef $functionDef,
): void {
$this->resetFunction();
$this->function = $this->parseIdentifier($function->name);
$this->functionDef = $functionDef;
foreach ($function->params as $index => $parameter) {
if ($parameter->default === null || !isset($functionDef->argInfoList[$index])) {
continue;
}
$argument = $functionDef->argInfoList[$index];
$argument->default = '';
$argument->arrayInitPlan = null;
$this->lowerArgumentDefault($parameter, $argument);
}
}
private function finalizePreparedProperty(PropertyDef $property, Node\Expr $expression): void
{
$this->resetFunction();
$property->arrayInitPlan = null;
if ($expression instanceof Node\Expr\Array_) {
$property->arrayInitPlan = $this->buildLiteralArrayInitPlan($expression);
$property->default = $property->arrayInitPlan->expr;
} else {
$property->default = $this->parseIdentifier($expression);
}
}
private function finalizePreparedConstant(ConstantDef $constant, Node\Expr $expression): void
{
$this->resetFunction();
$constant->arrayExpr = '';
$constant->value = $this->parseIdentifier($expression);
if ($this->context->beforeStmtLines) {
if ($this->context->localVars) {
$constant->arrayExpr .= $this->genScopeVarDecl();
}
$constant->arrayExpr .= $this->parseBeforeStmtLines();
}
$constant->codegenFinalized = true;
}
private function finalizeGlobalConstantExpressions(Node\Stmt\Const_ $statement): void
{
foreach ($statement->consts as $constant) {
$name = $this->parseIdentifier($constant->name);
if ($this->namespace !== '') {
$name = $this->namespace . '\\' . $name;
}
$key = $this->escapeConstVar($name);
if (!isset($this->constants[$key])) {
continue;
}
$this->resetFunction();
$this->constants[$key]->value = $this->parseIdentifier($constant->value);
$this->constants[$key]->codegenFinalized = true;
}
}
/** @param array<Node> $stmts */
private function hasLibraryImportAnnotation(array $stmts): bool
{
@ -679,28 +896,10 @@ class Preprocessor extends CompilerBase
$list[] = $this->genArgumentDeclaration($argInfo);
}
if ($param->default) {
$arrayInitPlan = $param->default instanceof Node\Expr\Array_
? $this->withoutLocalClassEntryHoisting(
fn (): ArrayInitPlan => $this->buildLiteralArrayInitPlan($param->default),
)
: null;
if ($param->byRef) {
if ($this->isEmptyArray($param->default)) {
$argInfo->default = 'php::getEmptyArrayRef()';
$argInfo->defaultValue = null;
} elseif ($this->isNull($param->default)) {
$argInfo->default = 'nullptr';
$argInfo->defaultValue = null;
} elseif ($arrayInitPlan) {
$argInfo->default = 'php::newReference(' . $arrayInitPlan->expr . ')';
$argInfo->arrayInitPlan = $arrayInitPlan;
} else {
$argInfo->default = 'php::newReference(' . $this->parseParamDefaultValue($param->default) . ')';
}
} else {
$argInfo->default = $arrayInitPlan ? $arrayInitPlan->expr : $this->parseParamDefaultValue($param->default);
$argInfo->arrayInitPlan = $arrayInitPlan;
$argInfo->defaultValue = $param->default;
$argInfo->defaultExpr = $param->default;
$argInfo->defaultValue = $param->default;
if ($this->compilerPhase === self::PHASE_CONVERT) {
$this->lowerArgumentDefault($param, $argInfo);
}
} elseif ($param->variadic) {
// 变长参数可以视为空数组默认值
@ -713,6 +912,33 @@ class Preprocessor extends CompilerBase
$functionDef->argCountRequired = $lastRequiredIndex + 1;
}
protected function lowerArgumentDefault(Node\Param $param, ArgInfo $argInfo): void
{
if ($param->default === null) {
return;
}
$arrayInitPlan = $param->default instanceof Node\Expr\Array_
? $this->withoutLocalClassEntryHoisting(
fn (): ArrayInitPlan => $this->buildLiteralArrayInitPlan($param->default),
)
: null;
$argInfo->arrayInitPlan = $arrayInitPlan;
if ($param->byRef) {
if ($this->isEmptyArray($param->default)) {
$argInfo->default = 'php::getEmptyArrayRef()';
return;
}
if ($this->isNull($param->default)) {
$argInfo->default = 'nullptr';
return;
}
$value = $arrayInitPlan?->expr ?? $this->parseParamDefaultValue($param->default);
$argInfo->default = 'php::newReference(' . $value . ')';
return;
}
$argInfo->default = $arrayInitPlan?->expr ?? $this->parseParamDefaultValue($param->default);
}
protected function getFunctionDisplayName(FunctionDef $functionDef): string
{
if ($this->class) {
@ -1398,13 +1624,16 @@ class Preprocessor extends CompilerBase
private function parseClassLikeConstant(Node\Const_ $const, int $flags, string $type, string $class = '', ?string $declaredType = null): ConstantDef
{
$constName = $this->parseIdentifier($const->name);
$constValue = $this->parseIdentifier($const->value);
$constValue = $this->compilerPhase === self::PHASE_CONVERT
? $this->parseIdentifier($const->value)
: '';
$constInfo = new ConstantDef($constName, $flags, $type, $constValue);
$constInfo->valueExpr = $const->value;
$constInfo->declaredType = $declaredType;
$constInfo->codegenFinalized = $this->compilerPhase === self::PHASE_CONVERT;
if ($this->context->beforeStmtLines) {
if ($constInfo->codegenFinalized && $this->context->beforeStmtLines) {
$arrayExpr = '';
if ($this->context->localVars) {
$arrayExpr .= $this->genScopeVarDecl();
@ -1455,8 +1684,10 @@ class Preprocessor extends CompilerBase
if ($defaultNode !== null) {
$this->checkPropertyDefaultType($name, $typeNode, $defaultNode, $errorNode);
if ($defaultNode instanceof Node\Expr\Array_) {
$arrayInitPlan = $this->buildLiteralArrayInitPlan($defaultNode);
$default = $arrayInitPlan->expr;
if ($this->compilerPhase === self::PHASE_CONVERT) {
$arrayInitPlan = $this->buildLiteralArrayInitPlan($defaultNode);
$default = $arrayInitPlan->expr;
}
// Only narrow the property type to `array` when the declared type
// cannot already hold an array. `mixed`/`iterable`/union/nullable
// types are represented as php::Var and can legally store an array,
@ -1465,7 +1696,7 @@ class Preprocessor extends CompilerBase
if ($type !== Type::VAR) {
$type = Type::ARRAY;
}
} else {
} elseif ($this->compilerPhase === self::PHASE_CONVERT) {
$default = $this->parseIdentifier($defaultNode);
}
}
@ -1489,6 +1720,7 @@ class Preprocessor extends CompilerBase
}
$propDef->readonly = (bool) (($flags | $this->classDef->flags) & Modifiers::READONLY);
$propDef->class = $class;
$propDef->defaultExpr = $defaultNode;
$propDef->arrayInitPlan = $arrayInitPlan;
$propDef->requiresRuntimeDefaultInit = $this->propertyDefaultRequiresRuntimeInit($defaultNode);
$propDef->promoted = $promoted;

@ -17,7 +17,9 @@ trait DeclarationSymbolTrait
{
foreach ($v2->consts as $const) {
$name = $this->parseIdentifier($const->name);
$value = $this->parseIdentifier($const->value);
$value = $this->compilerPhase === self::PHASE_CONVERT
? $this->parseIdentifier($const->value)
: '';
if ($this->namespace) {
$name = $this->namespace . '\\' . $name;
}
@ -30,7 +32,10 @@ trait DeclarationSymbolTrait
$constInfo = new \stdClass();
$constInfo->value = $value;
$constInfo->valueExpr = $valueExpr;
$constInfo->type = $this->detectStrValueType($value);
$constInfo->type = $this->compilerPhase === self::PHASE_CONVERT
? $this->detectStrValueType($value)
: Type::VAR;
$constInfo->codegenFinalized = $this->compilerPhase === self::PHASE_CONVERT;
$constInfo->namespace = $this->namespace;
$constInfo->name = $name;
$this->constants[$this->escapeConstVar($name)] = $constInfo;

@ -537,6 +537,9 @@ class Translator extends Preprocessor
{
$previousPhase = $this->enterCompilerPhase(self::PHASE_CONVERT);
try {
if (!$this->declarationExpressionsFinalized) {
$this->finalizeDeclarationExpressions(array_keys($this->preparedFileAsts));
}
$file = realpath($file);
$phpCode = $this->loadFile($file);
$this->localHeaders = [];
@ -777,13 +780,19 @@ class Translator extends Preprocessor
$lines[] = $pythonModuleDeclarations;
}
$lines[] = 'zend_class_entry *get_class(int class_id, const php::Str &class_name);';
$lines[] = 'zend_function *get_func(int func_id, const php::Str &func_name);';
$lines[] = 'zend_function *get_method(int func_id, const php::Str &method_name, int class_id, const php::Str &class_name);';
$lines[] = 'zend_class_entry *get_persistent_class(int class_id, const php::Str &class_name);';
$lines[] = 'zend_function *get_persistent_func(int func_id, const php::Str &func_name);';
$lines[] = 'zend_function *get_persistent_method(int func_id, const php::Str &method_name, int class_id, const php::Str &class_name);';
$lines[] = 'uint32_t get_persistent_prop(int prop_id, const php::Str &prop_name, int class_id, const php::Str &class_name);' . PHP_EOL;
$lines[] = 'enum class RequestClassId : uint32_t {};';
$lines[] = 'enum class PersistentClassId : uint32_t {};';
$lines[] = 'enum class RequestFuncId : uint32_t {};';
$lines[] = 'enum class PersistentFuncId : uint32_t {};';
$lines[] = 'enum class PersistentPropertyId : uint32_t {};' . PHP_EOL;
$lines[] = 'zend_class_entry *get_class(RequestClassId class_id, const php::Str &class_name);';
$lines[] = 'zend_function *get_func(RequestFuncId func_id, const php::Str &func_name);';
$lines[] = 'zend_function *get_method(RequestFuncId func_id, const php::Str &method_name, RequestClassId class_id, const php::Str &class_name);';
$lines[] = 'zend_class_entry *get_persistent_class(PersistentClassId class_id, const php::Str &class_name);';
$lines[] = 'zend_function *get_persistent_func(PersistentFuncId func_id, const php::Str &func_name);';
$lines[] = 'zend_function *get_persistent_method(PersistentFuncId func_id, const php::Str &method_name, PersistentClassId class_id, const php::Str &class_name);';
$lines[] = 'uint32_t get_persistent_prop(PersistentPropertyId prop_id, const php::Str &prop_name, const php::Str &class_name);' . PHP_EOL;
foreach ($this->getClassLikesWithConstants() as $classDef) {
foreach ($classDef->constants as $constant) {
@ -802,6 +811,16 @@ class Translator extends Preprocessor
}
public function genExtension(): string
{
$previousPhase = $this->enterCompilerPhase(self::PHASE_CONVERT);
try {
return $this->doGenExtension();
} finally {
$this->restoreCompilerPhase($previousPhase);
}
}
private function doGenExtension(): string
{
if ($this->isBuildModeBin()) {
if (!$this->hasFunction(self::ENTRY_FUNCTION)) {
@ -876,49 +895,56 @@ class Translator extends Preprocessor
$code .= "// functions \n";
$code .= <<<'CODE'
zend_class_entry *get_class(int class_id, const php::Str &class_name) {
if (UNEXPECTED(php_class_map[class_id] == nullptr)) {
php_class_map[class_id] = php::getClassEntrySafe(class_name);
zend_class_entry *get_class(RequestClassId class_id, const php::Str &class_name) {
const auto index = static_cast<uint32_t>(class_id);
if (UNEXPECTED(php_class_map[index] == nullptr)) {
php_class_map[index] = php::getClassEntrySafe(class_name);
}
return php_class_map[class_id];
return php_class_map[index];
}
zend_function *get_func(int func_id, const php::Str &func_name) {
if (UNEXPECTED(php_func_map[func_id] == nullptr)) {
php_func_map[func_id] = php::getFunction(func_name);
zend_function *get_func(RequestFuncId func_id, const php::Str &func_name) {
const auto index = static_cast<uint32_t>(func_id);
if (UNEXPECTED(php_func_map[index] == nullptr)) {
php_func_map[index] = php::getFunction(func_name);
}
return php_func_map[func_id];
return php_func_map[index];
}
zend_function *get_method(int func_id, const php::Str &method_name, int class_id, const php::Str &class_name) {
if (UNEXPECTED(php_func_map[func_id] == nullptr)) {
zend_function *get_method(RequestFuncId func_id, const php::Str &method_name, RequestClassId class_id, const php::Str &class_name) {
const auto index = static_cast<uint32_t>(func_id);
if (UNEXPECTED(php_func_map[index] == nullptr)) {
auto ce = get_class(class_id, class_name);
php_func_map[func_id] = php::getMethod(ce, method_name);
php_func_map[index] = php::getMethod(ce, method_name);
}
return php_func_map[func_id];
return php_func_map[index];
}
zend_class_entry *get_persistent_class(int class_id, const php::Str &class_name) {
return php::getPersistentCache(php_persistent_class_map[class_id], [&]() {
zend_class_entry *get_persistent_class(PersistentClassId class_id, const php::Str &class_name) {
const auto index = static_cast<uint32_t>(class_id);
return php::getPersistentCache(php_persistent_class_map[index], [&]() {
return php::getClassEntrySafe(class_name);
});
}
zend_function *get_persistent_func(int func_id, const php::Str &func_name) {
return php::getPersistentCache(php_persistent_func_map[func_id], [&]() {
zend_function *get_persistent_func(PersistentFuncId func_id, const php::Str &func_name) {
const auto index = static_cast<uint32_t>(func_id);
return php::getPersistentCache(php_persistent_func_map[index], [&]() {
return php::getFunction(func_name);
});
}
zend_function *get_persistent_method(int func_id, const php::Str &method_name, int class_id, const php::Str &class_name) {
return php::getPersistentCache(php_persistent_func_map[func_id], [&]() {
zend_function *get_persistent_method(PersistentFuncId func_id, const php::Str &method_name, PersistentClassId class_id, const php::Str &class_name) {
const auto index = static_cast<uint32_t>(func_id);
return php::getPersistentCache(php_persistent_func_map[index], [&]() {
auto ce = get_persistent_class(class_id, class_name);
return php::getMethod(ce, method_name);
});
}
uint32_t get_persistent_prop(int prop_id, const php::Str &prop_name, int class_id, const php::Str &class_name) {
auto value = php::getPersistentCache(php_persistent_property_map[prop_id], [&]() {
uint32_t get_persistent_prop(PersistentPropertyId prop_id, const php::Str &prop_name, const php::Str &class_name) {
const auto index = static_cast<uint32_t>(prop_id);
auto value = php::getPersistentCache(php_persistent_property_map[index], [&]() {
return php::getPropertyOffset(class_name, prop_name) + 1024;
});
return value - 1024;
@ -1847,7 +1873,7 @@ CODE;
. ' = ' . $this->genDefaultArgumentExpr($name, $argumentIndex);
} else {
$arg = $this->genArgumentDeclaration($argInfo);
if ($argInfo->default !== '' && !$this->isConstructorNativeFunction($func)) {
if ($argInfo->hasDefaultValue() && !$this->isConstructorNativeFunction($func)) {
$arg .= ' = ' . $this->genDefaultArgumentExpr($name, $argumentIndex);
}
}
@ -3709,7 +3735,7 @@ CODE;
$cppCode .= $this->getIndent() . '}' . PHP_EOL;
$cppCode .= $this->genExtraNamedVariadicArgs($var);
} else {
if ($argInfo->default !== '') {
if ($argInfo->hasDefaultValue()) {
$nativeName = str_starts_with($fn, self::PREFIX)
? substr($fn, strlen(self::PREFIX))
: $fn;

Loading…
Cancel
Save